/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: Vincent Ladeuil
  • Date: 2008-09-01 06:16:07 UTC
  • mto: (3668.1.1 trunk) (3703.1.1 trunk)
  • mto: This revision was merged to the branch mainline in revision 3669.
  • Revision ID: v.ladeuil+lp@free.fr-20080901061607-dgqfeswfmaeouvjp
Fix PEP8.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006, 2007, 2008 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, S_IEXEC
 
20
 
 
21
from bzrlib.lazy_import import lazy_import
 
22
lazy_import(globals(), """
 
23
from bzrlib import (
 
24
    annotate,
 
25
    bzrdir,
 
26
    delta,
 
27
    errors,
 
28
    inventory,
 
29
    osutils,
 
30
    revision as _mod_revision,
 
31
    )
 
32
""")
 
33
from bzrlib.errors import (DuplicateKey, MalformedTransform, NoSuchFile,
 
34
                           ReusingTransform, NotVersionedError, CantMoveRoot,
 
35
                           ExistingLimbo, ImmortalLimbo, NoFinalPath,
 
36
                           UnableCreateSymlink)
 
37
from bzrlib.inventory import InventoryEntry
 
38
from bzrlib.osutils import (
 
39
    delete_any,
 
40
    file_kind,
 
41
    has_symlinks,
 
42
    lexists,
 
43
    pathjoin,
 
44
    splitpath,
 
45
    supports_executable,
 
46
)
 
47
from bzrlib.progress import DummyProgress, ProgressPhase
 
48
from bzrlib.symbol_versioning import (
 
49
        deprecated_function,
 
50
        )
 
51
from bzrlib.trace import mutter, warning
 
52
from bzrlib import tree
 
53
import bzrlib.ui
 
54
import bzrlib.urlutils as urlutils
 
55
 
 
56
 
 
57
ROOT_PARENT = "root-parent"
 
58
 
 
59
 
 
60
def unique_add(map, key, value):
 
61
    if key in map:
 
62
        raise DuplicateKey(key=key)
 
63
    map[key] = value
 
64
 
 
65
 
 
66
class _TransformResults(object):
 
67
    def __init__(self, modified_paths, rename_count):
 
68
        object.__init__(self)
 
69
        self.modified_paths = modified_paths
 
70
        self.rename_count = rename_count
 
71
 
 
72
 
 
73
class TreeTransformBase(object):
 
74
    """The base class for TreeTransform and TreeTransformBase"""
 
75
 
 
76
    def __init__(self, tree, limbodir, pb=DummyProgress(),
 
77
                 case_sensitive=True):
 
78
        """Constructor.
 
79
 
 
80
        :param tree: The tree that will be transformed, but not necessarily
 
81
            the output tree.
 
82
        :param limbodir: A directory where new files can be stored until
 
83
            they are installed in their proper places
 
84
        :param pb: A ProgressBar indicating how much progress is being made
 
85
        :param case_sensitive: If True, the target of the transform is
 
86
            case sensitive, not just case preserving.
 
87
        """
 
88
        object.__init__(self)
 
89
        self._tree = tree
 
90
        self._limbodir = limbodir
 
91
        self._deletiondir = None
 
92
        self._id_number = 0
 
93
        # mapping of trans_id -> new basename
 
94
        self._new_name = {}
 
95
        # mapping of trans_id -> new parent trans_id
 
96
        self._new_parent = {}
 
97
        # mapping of trans_id with new contents -> new file_kind
 
98
        self._new_contents = {}
 
99
        # A mapping of transform ids to their limbo filename
 
100
        self._limbo_files = {}
 
101
        # A mapping of transform ids to a set of the transform ids of children
 
102
        # that their limbo directory has
 
103
        self._limbo_children = {}
 
104
        # Map transform ids to maps of child filename to child transform id
 
105
        self._limbo_children_names = {}
 
106
        # List of transform ids that need to be renamed from limbo into place
 
107
        self._needs_rename = set()
 
108
        # Set of trans_ids whose contents will be removed
 
109
        self._removed_contents = set()
 
110
        # Mapping of trans_id -> new execute-bit value
 
111
        self._new_executability = {}
 
112
        # Mapping of trans_id -> new tree-reference value
 
113
        self._new_reference_revision = {}
 
114
        # Mapping of trans_id -> new file_id
 
115
        self._new_id = {}
 
116
        # Mapping of old file-id -> trans_id
 
117
        self._non_present_ids = {}
 
118
        # Mapping of new file_id -> trans_id
 
119
        self._r_new_id = {}
 
120
        # Set of file_ids that will be removed
 
121
        self._removed_id = set()
 
122
        # Mapping of path in old tree -> trans_id
 
123
        self._tree_path_ids = {}
 
124
        # Mapping trans_id -> path in old tree
 
125
        self._tree_id_paths = {}
 
126
        # Cache of realpath results, to speed up canonical_path
 
127
        self._realpaths = {}
 
128
        # Cache of relpath results, to speed up canonical_path
 
129
        self._relpaths = {}
 
130
        # The trans_id that will be used as the tree root
 
131
        self._new_root = self.trans_id_tree_file_id(tree.get_root_id())
 
132
        # Indictor of whether the transform has been applied
 
133
        self._done = False
 
134
        # A progress bar
 
135
        self._pb = pb
 
136
        # Whether the target is case sensitive
 
137
        self._case_sensitive_target = case_sensitive
 
138
        # A counter of how many files have been renamed
 
139
        self.rename_count = 0
 
140
 
 
141
    def __get_root(self):
 
142
        return self._new_root
 
143
 
 
144
    root = property(__get_root)
 
145
 
 
146
    def finalize(self):
 
147
        """Release the working tree lock, if held, clean up limbo dir.
 
148
 
 
149
        This is required if apply has not been invoked, but can be invoked
 
150
        even after apply.
 
151
        """
 
152
        if self._tree is None:
 
153
            return
 
154
        try:
 
155
            entries = [(self._limbo_name(t), t, k) for t, k in
 
156
                       self._new_contents.iteritems()]
 
157
            entries.sort(reverse=True)
 
158
            for path, trans_id, kind in entries:
 
159
                if kind == "directory":
 
160
                    os.rmdir(path)
 
161
                else:
 
162
                    os.unlink(path)
 
163
            try:
 
164
                os.rmdir(self._limbodir)
 
165
            except OSError:
 
166
                # We don't especially care *why* the dir is immortal.
 
167
                raise ImmortalLimbo(self._limbodir)
 
168
            try:
 
169
                if self._deletiondir is not None:
 
170
                    os.rmdir(self._deletiondir)
 
171
            except OSError:
 
172
                raise errors.ImmortalPendingDeletion(self._deletiondir)
 
173
        finally:
 
174
            self._tree.unlock()
 
175
            self._tree = None
 
176
 
 
177
    def _assign_id(self):
 
178
        """Produce a new tranform id"""
 
179
        new_id = "new-%s" % self._id_number
 
180
        self._id_number +=1
 
181
        return new_id
 
182
 
 
183
    def create_path(self, name, parent):
 
184
        """Assign a transaction id to a new path"""
 
185
        trans_id = self._assign_id()
 
186
        unique_add(self._new_name, trans_id, name)
 
187
        unique_add(self._new_parent, trans_id, parent)
 
188
        return trans_id
 
189
 
 
190
    def adjust_path(self, name, parent, trans_id):
 
191
        """Change the path that is assigned to a transaction id."""
 
192
        if trans_id == self._new_root:
 
193
            raise CantMoveRoot
 
194
        previous_parent = self._new_parent.get(trans_id)
 
195
        previous_name = self._new_name.get(trans_id)
 
196
        self._new_name[trans_id] = name
 
197
        self._new_parent[trans_id] = parent
 
198
        if (trans_id in self._limbo_files and
 
199
            trans_id not in self._needs_rename):
 
200
            self._rename_in_limbo([trans_id])
 
201
            self._limbo_children[previous_parent].remove(trans_id)
 
202
            del self._limbo_children_names[previous_parent][previous_name]
 
203
 
 
204
    def _rename_in_limbo(self, trans_ids):
 
205
        """Fix limbo names so that the right final path is produced.
 
206
 
 
207
        This means we outsmarted ourselves-- we tried to avoid renaming
 
208
        these files later by creating them with their final names in their
 
209
        final parents.  But now the previous name or parent is no longer
 
210
        suitable, so we have to rename them.
 
211
 
 
212
        Even for trans_ids that have no new contents, we must remove their
 
213
        entries from _limbo_files, because they are now stale.
 
214
        """
 
215
        for trans_id in trans_ids:
 
216
            old_path = self._limbo_files.pop(trans_id)
 
217
            if trans_id not in self._new_contents:
 
218
                continue
 
219
            new_path = self._limbo_name(trans_id)
 
220
            os.rename(old_path, new_path)
 
221
 
 
222
    def adjust_root_path(self, name, parent):
 
223
        """Emulate moving the root by moving all children, instead.
 
224
        
 
225
        We do this by undoing the association of root's transaction id with the
 
226
        current tree.  This allows us to create a new directory with that
 
227
        transaction id.  We unversion the root directory and version the 
 
228
        physically new directory, and hope someone versions the tree root
 
229
        later.
 
230
        """
 
231
        old_root = self._new_root
 
232
        old_root_file_id = self.final_file_id(old_root)
 
233
        # force moving all children of root
 
234
        for child_id in self.iter_tree_children(old_root):
 
235
            if child_id != parent:
 
236
                self.adjust_path(self.final_name(child_id), 
 
237
                                 self.final_parent(child_id), child_id)
 
238
            file_id = self.final_file_id(child_id)
 
239
            if file_id is not None:
 
240
                self.unversion_file(child_id)
 
241
            self.version_file(file_id, child_id)
 
242
        
 
243
        # the physical root needs a new transaction id
 
244
        self._tree_path_ids.pop("")
 
245
        self._tree_id_paths.pop(old_root)
 
246
        self._new_root = self.trans_id_tree_file_id(self._tree.get_root_id())
 
247
        if parent == old_root:
 
248
            parent = self._new_root
 
249
        self.adjust_path(name, parent, old_root)
 
250
        self.create_directory(old_root)
 
251
        self.version_file(old_root_file_id, old_root)
 
252
        self.unversion_file(self._new_root)
 
253
 
 
254
    def trans_id_tree_file_id(self, inventory_id):
 
255
        """Determine the transaction id of a working tree file.
 
256
        
 
257
        This reflects only files that already exist, not ones that will be
 
258
        added by transactions.
 
259
        """
 
260
        path = self._tree.id2path(inventory_id)
 
261
        return self.trans_id_tree_path(path)
 
262
 
 
263
    def trans_id_file_id(self, file_id):
 
264
        """Determine or set the transaction id associated with a file ID.
 
265
        A new id is only created for file_ids that were never present.  If
 
266
        a transaction has been unversioned, it is deliberately still returned.
 
267
        (this will likely lead to an unversioned parent conflict.)
 
268
        """
 
269
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
 
270
            return self._r_new_id[file_id]
 
271
        elif file_id in self._tree.inventory:
 
272
            return self.trans_id_tree_file_id(file_id)
 
273
        elif file_id in self._non_present_ids:
 
274
            return self._non_present_ids[file_id]
 
275
        else:
 
276
            trans_id = self._assign_id()
 
277
            self._non_present_ids[file_id] = trans_id
 
278
            return trans_id
 
279
 
 
280
    def canonical_path(self, path):
 
281
        """Get the canonical tree-relative path"""
 
282
        # don't follow final symlinks
 
283
        abs = self._tree.abspath(path)
 
284
        if abs in self._relpaths:
 
285
            return self._relpaths[abs]
 
286
        dirname, basename = os.path.split(abs)
 
287
        if dirname not in self._realpaths:
 
288
            self._realpaths[dirname] = os.path.realpath(dirname)
 
289
        dirname = self._realpaths[dirname]
 
290
        abs = pathjoin(dirname, basename)
 
291
        if dirname in self._relpaths:
 
292
            relpath = pathjoin(self._relpaths[dirname], basename)
 
293
            relpath = relpath.rstrip('/\\')
 
294
        else:
 
295
            relpath = self._tree.relpath(abs)
 
296
        self._relpaths[abs] = relpath
 
297
        return relpath
 
298
 
 
299
    def trans_id_tree_path(self, path):
 
300
        """Determine (and maybe set) the transaction ID for a tree path."""
 
301
        path = self.canonical_path(path)
 
302
        if path not in self._tree_path_ids:
 
303
            self._tree_path_ids[path] = self._assign_id()
 
304
            self._tree_id_paths[self._tree_path_ids[path]] = path
 
305
        return self._tree_path_ids[path]
 
306
 
 
307
    def get_tree_parent(self, trans_id):
 
308
        """Determine id of the parent in the tree."""
 
309
        path = self._tree_id_paths[trans_id]
 
310
        if path == "":
 
311
            return ROOT_PARENT
 
312
        return self.trans_id_tree_path(os.path.dirname(path))
 
313
 
 
314
    def create_file(self, contents, trans_id, mode_id=None):
 
315
        """Schedule creation of a new file.
 
316
 
 
317
        See also new_file.
 
318
        
 
319
        Contents is an iterator of strings, all of which will be written
 
320
        to the target destination.
 
321
 
 
322
        New file takes the permissions of any existing file with that id,
 
323
        unless mode_id is specified.
 
324
        """
 
325
        name = self._limbo_name(trans_id)
 
326
        f = open(name, 'wb')
 
327
        try:
 
328
            try:
 
329
                unique_add(self._new_contents, trans_id, 'file')
 
330
            except:
 
331
                # Clean up the file, it never got registered so
 
332
                # TreeTransform.finalize() won't clean it up.
 
333
                f.close()
 
334
                os.unlink(name)
 
335
                raise
 
336
 
 
337
            f.writelines(contents)
 
338
        finally:
 
339
            f.close()
 
340
        self._set_mode(trans_id, mode_id, S_ISREG)
 
341
 
 
342
    def _set_mode(self, trans_id, mode_id, typefunc):
 
343
        """Set the mode of new file contents.
 
344
        The mode_id is the existing file to get the mode from (often the same
 
345
        as trans_id).  The operation is only performed if there's a mode match
 
346
        according to typefunc.
 
347
        """
 
348
        if mode_id is None:
 
349
            mode_id = trans_id
 
350
        try:
 
351
            old_path = self._tree_id_paths[mode_id]
 
352
        except KeyError:
 
353
            return
 
354
        try:
 
355
            mode = os.stat(self._tree.abspath(old_path)).st_mode
 
356
        except OSError, e:
 
357
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
 
358
                # Either old_path doesn't exist, or the parent of the
 
359
                # target is not a directory (but will be one eventually)
 
360
                # Either way, we know it doesn't exist *right now*
 
361
                # See also bug #248448
 
362
                return
 
363
            else:
 
364
                raise
 
365
        if typefunc(mode):
 
366
            os.chmod(self._limbo_name(trans_id), mode)
 
367
 
 
368
    def create_hardlink(self, path, trans_id):
 
369
        """Schedule creation of a hard link"""
 
370
        name = self._limbo_name(trans_id)
 
371
        try:
 
372
            os.link(path, name)
 
373
        except OSError, e:
 
374
            if e.errno != errno.EPERM:
 
375
                raise
 
376
            raise errors.HardLinkNotSupported(path)
 
377
        try:
 
378
            unique_add(self._new_contents, trans_id, 'file')
 
379
        except:
 
380
            # Clean up the file, it never got registered so
 
381
            # TreeTransform.finalize() won't clean it up.
 
382
            os.unlink(name)
 
383
            raise
 
384
 
 
385
    def create_directory(self, trans_id):
 
386
        """Schedule creation of a new directory.
 
387
        
 
388
        See also new_directory.
 
389
        """
 
390
        os.mkdir(self._limbo_name(trans_id))
 
391
        unique_add(self._new_contents, trans_id, 'directory')
 
392
 
 
393
    def create_symlink(self, target, trans_id):
 
394
        """Schedule creation of a new symbolic link.
 
395
 
 
396
        target is a bytestring.
 
397
        See also new_symlink.
 
398
        """
 
399
        if has_symlinks():
 
400
            os.symlink(target, self._limbo_name(trans_id))
 
401
            unique_add(self._new_contents, trans_id, 'symlink')
 
402
        else:
 
403
            try:
 
404
                path = FinalPaths(self).get_path(trans_id)
 
405
            except KeyError:
 
406
                path = None
 
407
            raise UnableCreateSymlink(path=path)
 
408
 
 
409
    def cancel_creation(self, trans_id):
 
410
        """Cancel the creation of new file contents."""
 
411
        del self._new_contents[trans_id]
 
412
        children = self._limbo_children.get(trans_id)
 
413
        # if this is a limbo directory with children, move them before removing
 
414
        # the directory
 
415
        if children is not None:
 
416
            self._rename_in_limbo(children)
 
417
            del self._limbo_children[trans_id]
 
418
            del self._limbo_children_names[trans_id]
 
419
        delete_any(self._limbo_name(trans_id))
 
420
 
 
421
    def delete_contents(self, trans_id):
 
422
        """Schedule the contents of a path entry for deletion"""
 
423
        self.tree_kind(trans_id)
 
424
        self._removed_contents.add(trans_id)
 
425
 
 
426
    def cancel_deletion(self, trans_id):
 
427
        """Cancel a scheduled deletion"""
 
428
        self._removed_contents.remove(trans_id)
 
429
 
 
430
    def unversion_file(self, trans_id):
 
431
        """Schedule a path entry to become unversioned"""
 
432
        self._removed_id.add(trans_id)
 
433
 
 
434
    def delete_versioned(self, trans_id):
 
435
        """Delete and unversion a versioned file"""
 
436
        self.delete_contents(trans_id)
 
437
        self.unversion_file(trans_id)
 
438
 
 
439
    def set_executability(self, executability, trans_id):
 
440
        """Schedule setting of the 'execute' bit
 
441
        To unschedule, set to None
 
442
        """
 
443
        if executability is None:
 
444
            del self._new_executability[trans_id]
 
445
        else:
 
446
            unique_add(self._new_executability, trans_id, executability)
 
447
 
 
448
    def set_tree_reference(self, revision_id, trans_id):
 
449
        """Set the reference associated with a directory"""
 
450
        unique_add(self._new_reference_revision, trans_id, revision_id)
 
451
 
 
452
    def version_file(self, file_id, trans_id):
 
453
        """Schedule a file to become versioned."""
 
454
        if file_id is None:
 
455
            raise ValueError()
 
456
        unique_add(self._new_id, trans_id, file_id)
 
457
        unique_add(self._r_new_id, file_id, trans_id)
 
458
 
 
459
    def cancel_versioning(self, trans_id):
 
460
        """Undo a previous versioning of a file"""
 
461
        file_id = self._new_id[trans_id]
 
462
        del self._new_id[trans_id]
 
463
        del self._r_new_id[file_id]
 
464
 
 
465
    def new_paths(self, filesystem_only=False):
 
466
        """Determine the paths of all new and changed files.
 
467
 
 
468
        :param filesystem_only: if True, only calculate values for files
 
469
            that require renames or execute bit changes.
 
470
        """
 
471
        new_ids = set()
 
472
        if filesystem_only:
 
473
            stale_ids = self._needs_rename.difference(self._new_name)
 
474
            stale_ids.difference_update(self._new_parent)
 
475
            stale_ids.difference_update(self._new_contents)
 
476
            stale_ids.difference_update(self._new_id)
 
477
            needs_rename = self._needs_rename.difference(stale_ids)
 
478
            id_sets = (needs_rename, self._new_executability)
 
479
        else:
 
480
            id_sets = (self._new_name, self._new_parent, self._new_contents,
 
481
                       self._new_id, self._new_executability)
 
482
        for id_set in id_sets:
 
483
            new_ids.update(id_set)
 
484
        return sorted(FinalPaths(self).get_paths(new_ids))
 
485
 
 
486
    def _inventory_altered(self):
 
487
        """Get the trans_ids and paths of files needing new inv entries."""
 
488
        new_ids = set()
 
489
        for id_set in [self._new_name, self._new_parent, self._new_id,
 
490
                       self._new_executability]:
 
491
            new_ids.update(id_set)
 
492
        changed_kind = set(self._removed_contents)
 
493
        changed_kind.intersection_update(self._new_contents)
 
494
        changed_kind.difference_update(new_ids)
 
495
        changed_kind = (t for t in changed_kind if self.tree_kind(t) !=
 
496
                        self.final_kind(t))
 
497
        new_ids.update(changed_kind)
 
498
        return sorted(FinalPaths(self).get_paths(new_ids))
 
499
 
 
500
    def tree_kind(self, trans_id):
 
501
        """Determine the file kind in the working tree.
 
502
 
 
503
        Raises NoSuchFile if the file does not exist
 
504
        """
 
505
        path = self._tree_id_paths.get(trans_id)
 
506
        if path is None:
 
507
            raise NoSuchFile(None)
 
508
        try:
 
509
            return file_kind(self._tree.abspath(path))
 
510
        except OSError, e:
 
511
            if e.errno != errno.ENOENT:
 
512
                raise
 
513
            else:
 
514
                raise NoSuchFile(path)
 
515
 
 
516
    def final_kind(self, trans_id):
 
517
        """Determine the final file kind, after any changes applied.
 
518
        
 
519
        Raises NoSuchFile if the file does not exist/has no contents.
 
520
        (It is conceivable that a path would be created without the
 
521
        corresponding contents insertion command)
 
522
        """
 
523
        if trans_id in self._new_contents:
 
524
            return self._new_contents[trans_id]
 
525
        elif trans_id in self._removed_contents:
 
526
            raise NoSuchFile(None)
 
527
        else:
 
528
            return self.tree_kind(trans_id)
 
529
 
 
530
    def tree_file_id(self, trans_id):
 
531
        """Determine the file id associated with the trans_id in the tree"""
 
532
        try:
 
533
            path = self._tree_id_paths[trans_id]
 
534
        except KeyError:
 
535
            # the file is a new, unversioned file, or invalid trans_id
 
536
            return None
 
537
        # the file is old; the old id is still valid
 
538
        if self._new_root == trans_id:
 
539
            return self._tree.get_root_id()
 
540
        return self._tree.inventory.path2id(path)
 
541
 
 
542
    def final_file_id(self, trans_id):
 
543
        """Determine the file id after any changes are applied, or None.
 
544
        
 
545
        None indicates that the file will not be versioned after changes are
 
546
        applied.
 
547
        """
 
548
        try:
 
549
            return self._new_id[trans_id]
 
550
        except KeyError:
 
551
            if trans_id in self._removed_id:
 
552
                return None
 
553
        return self.tree_file_id(trans_id)
 
554
 
 
555
    def inactive_file_id(self, trans_id):
 
556
        """Return the inactive file_id associated with a transaction id.
 
557
        That is, the one in the tree or in non_present_ids.
 
558
        The file_id may actually be active, too.
 
559
        """
 
560
        file_id = self.tree_file_id(trans_id)
 
561
        if file_id is not None:
 
562
            return file_id
 
563
        for key, value in self._non_present_ids.iteritems():
 
564
            if value == trans_id:
 
565
                return key
 
566
 
 
567
    def final_parent(self, trans_id):
 
568
        """Determine the parent file_id, after any changes are applied.
 
569
 
 
570
        ROOT_PARENT is returned for the tree root.
 
571
        """
 
572
        try:
 
573
            return self._new_parent[trans_id]
 
574
        except KeyError:
 
575
            return self.get_tree_parent(trans_id)
 
576
 
 
577
    def final_name(self, trans_id):
 
578
        """Determine the final filename, after all changes are applied."""
 
579
        try:
 
580
            return self._new_name[trans_id]
 
581
        except KeyError:
 
582
            try:
 
583
                return os.path.basename(self._tree_id_paths[trans_id])
 
584
            except KeyError:
 
585
                raise NoFinalPath(trans_id, self)
 
586
 
 
587
    def by_parent(self):
 
588
        """Return a map of parent: children for known parents.
 
589
        
 
590
        Only new paths and parents of tree files with assigned ids are used.
 
591
        """
 
592
        by_parent = {}
 
593
        items = list(self._new_parent.iteritems())
 
594
        items.extend((t, self.final_parent(t)) for t in 
 
595
                      self._tree_id_paths.keys())
 
596
        for trans_id, parent_id in items:
 
597
            if parent_id not in by_parent:
 
598
                by_parent[parent_id] = set()
 
599
            by_parent[parent_id].add(trans_id)
 
600
        return by_parent
 
601
 
 
602
    def path_changed(self, trans_id):
 
603
        """Return True if a trans_id's path has changed."""
 
604
        return (trans_id in self._new_name) or (trans_id in self._new_parent)
 
605
 
 
606
    def new_contents(self, trans_id):
 
607
        return (trans_id in self._new_contents)
 
608
 
 
609
    def find_conflicts(self):
 
610
        """Find any violations of inventory or filesystem invariants"""
 
611
        if self._done is True:
 
612
            raise ReusingTransform()
 
613
        conflicts = []
 
614
        # ensure all children of all existent parents are known
 
615
        # all children of non-existent parents are known, by definition.
 
616
        self._add_tree_children()
 
617
        by_parent = self.by_parent()
 
618
        conflicts.extend(self._unversioned_parents(by_parent))
 
619
        conflicts.extend(self._parent_loops())
 
620
        conflicts.extend(self._duplicate_entries(by_parent))
 
621
        conflicts.extend(self._duplicate_ids())
 
622
        conflicts.extend(self._parent_type_conflicts(by_parent))
 
623
        conflicts.extend(self._improper_versioning())
 
624
        conflicts.extend(self._executability_conflicts())
 
625
        conflicts.extend(self._overwrite_conflicts())
 
626
        return conflicts
 
627
 
 
628
    def _add_tree_children(self):
 
629
        """Add all the children of all active parents to the known paths.
 
630
 
 
631
        Active parents are those which gain children, and those which are
 
632
        removed.  This is a necessary first step in detecting conflicts.
 
633
        """
 
634
        parents = self.by_parent().keys()
 
635
        parents.extend([t for t in self._removed_contents if 
 
636
                        self.tree_kind(t) == 'directory'])
 
637
        for trans_id in self._removed_id:
 
638
            file_id = self.tree_file_id(trans_id)
 
639
            if file_id is not None:
 
640
                if self._tree.inventory[file_id].kind == 'directory':
 
641
                    parents.append(trans_id)
 
642
            elif self.tree_kind(trans_id) == 'directory':
 
643
                parents.append(trans_id)
 
644
 
 
645
        for parent_id in parents:
 
646
            # ensure that all children are registered with the transaction
 
647
            list(self.iter_tree_children(parent_id))
 
648
 
 
649
    def iter_tree_children(self, parent_id):
 
650
        """Iterate through the entry's tree children, if any"""
 
651
        try:
 
652
            path = self._tree_id_paths[parent_id]
 
653
        except KeyError:
 
654
            return
 
655
        try:
 
656
            children = os.listdir(self._tree.abspath(path))
 
657
        except OSError, e:
 
658
            if not (osutils._is_error_enotdir(e)
 
659
                    or e.errno in (errno.ENOENT, errno.ESRCH)):
 
660
                raise
 
661
            return
 
662
 
 
663
        for child in children:
 
664
            childpath = joinpath(path, child)
 
665
            if self._tree.is_control_filename(childpath):
 
666
                continue
 
667
            yield self.trans_id_tree_path(childpath)
 
668
 
 
669
    def has_named_child(self, by_parent, parent_id, name):
 
670
        try:
 
671
            children = by_parent[parent_id]
 
672
        except KeyError:
 
673
            children = []
 
674
        for child in children:
 
675
            if self.final_name(child) == name:
 
676
                return True
 
677
        try:
 
678
            path = self._tree_id_paths[parent_id]
 
679
        except KeyError:
 
680
            return False
 
681
        childpath = joinpath(path, name)
 
682
        child_id = self._tree_path_ids.get(childpath)
 
683
        if child_id is None:
 
684
            return lexists(self._tree.abspath(childpath))
 
685
        else:
 
686
            if self.final_parent(child_id) != parent_id:
 
687
                return False
 
688
            if child_id in self._removed_contents:
 
689
                # XXX What about dangling file-ids?
 
690
                return False
 
691
            else:
 
692
                return True
 
693
 
 
694
    def _parent_loops(self):
 
695
        """No entry should be its own ancestor"""
 
696
        conflicts = []
 
697
        for trans_id in self._new_parent:
 
698
            seen = set()
 
699
            parent_id = trans_id
 
700
            while parent_id is not ROOT_PARENT:
 
701
                seen.add(parent_id)
 
702
                try:
 
703
                    parent_id = self.final_parent(parent_id)
 
704
                except KeyError:
 
705
                    break
 
706
                if parent_id == trans_id:
 
707
                    conflicts.append(('parent loop', trans_id))
 
708
                if parent_id in seen:
 
709
                    break
 
710
        return conflicts
 
711
 
 
712
    def _unversioned_parents(self, by_parent):
 
713
        """If parent directories are versioned, children must be versioned."""
 
714
        conflicts = []
 
715
        for parent_id, children in by_parent.iteritems():
 
716
            if parent_id is ROOT_PARENT:
 
717
                continue
 
718
            if self.final_file_id(parent_id) is not None:
 
719
                continue
 
720
            for child_id in children:
 
721
                if self.final_file_id(child_id) is not None:
 
722
                    conflicts.append(('unversioned parent', parent_id))
 
723
                    break;
 
724
        return conflicts
 
725
 
 
726
    def _improper_versioning(self):
 
727
        """Cannot version a file with no contents, or a bad type.
 
728
        
 
729
        However, existing entries with no contents are okay.
 
730
        """
 
731
        conflicts = []
 
732
        for trans_id in self._new_id.iterkeys():
 
733
            try:
 
734
                kind = self.final_kind(trans_id)
 
735
            except NoSuchFile:
 
736
                conflicts.append(('versioning no contents', trans_id))
 
737
                continue
 
738
            if not InventoryEntry.versionable_kind(kind):
 
739
                conflicts.append(('versioning bad kind', trans_id, kind))
 
740
        return conflicts
 
741
 
 
742
    def _executability_conflicts(self):
 
743
        """Check for bad executability changes.
 
744
        
 
745
        Only versioned files may have their executability set, because
 
746
        1. only versioned entries can have executability under windows
 
747
        2. only files can be executable.  (The execute bit on a directory
 
748
           does not indicate searchability)
 
749
        """
 
750
        conflicts = []
 
751
        for trans_id in self._new_executability:
 
752
            if self.final_file_id(trans_id) is None:
 
753
                conflicts.append(('unversioned executability', trans_id))
 
754
            else:
 
755
                try:
 
756
                    non_file = self.final_kind(trans_id) != "file"
 
757
                except NoSuchFile:
 
758
                    non_file = True
 
759
                if non_file is True:
 
760
                    conflicts.append(('non-file executability', trans_id))
 
761
        return conflicts
 
762
 
 
763
    def _overwrite_conflicts(self):
 
764
        """Check for overwrites (not permitted on Win32)"""
 
765
        conflicts = []
 
766
        for trans_id in self._new_contents:
 
767
            try:
 
768
                self.tree_kind(trans_id)
 
769
            except NoSuchFile:
 
770
                continue
 
771
            if trans_id not in self._removed_contents:
 
772
                conflicts.append(('overwrite', trans_id,
 
773
                                 self.final_name(trans_id)))
 
774
        return conflicts
 
775
 
 
776
    def _duplicate_entries(self, by_parent):
 
777
        """No directory may have two entries with the same name."""
 
778
        conflicts = []
 
779
        if (self._new_name, self._new_parent) == ({}, {}):
 
780
            return conflicts
 
781
        for children in by_parent.itervalues():
 
782
            name_ids = [(self.final_name(t), t) for t in children]
 
783
            if not self._case_sensitive_target:
 
784
                name_ids = [(n.lower(), t) for n, t in name_ids]
 
785
            name_ids.sort()
 
786
            last_name = None
 
787
            last_trans_id = None
 
788
            for name, trans_id in name_ids:
 
789
                try:
 
790
                    kind = self.final_kind(trans_id)
 
791
                except NoSuchFile:
 
792
                    kind = None
 
793
                file_id = self.final_file_id(trans_id)
 
794
                if kind is None and file_id is None:
 
795
                    continue
 
796
                if name == last_name:
 
797
                    conflicts.append(('duplicate', last_trans_id, trans_id,
 
798
                    name))
 
799
                last_name = name
 
800
                last_trans_id = trans_id
 
801
        return conflicts
 
802
 
 
803
    def _duplicate_ids(self):
 
804
        """Each inventory id may only be used once"""
 
805
        conflicts = []
 
806
        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
 
807
                                self._removed_id))
 
808
        all_ids = self._tree.all_file_ids()
 
809
        active_tree_ids = all_ids.difference(removed_tree_ids)
 
810
        for trans_id, file_id in self._new_id.iteritems():
 
811
            if file_id in active_tree_ids:
 
812
                old_trans_id = self.trans_id_tree_file_id(file_id)
 
813
                conflicts.append(('duplicate id', old_trans_id, trans_id))
 
814
        return conflicts
 
815
 
 
816
    def _parent_type_conflicts(self, by_parent):
 
817
        """parents must have directory 'contents'."""
 
818
        conflicts = []
 
819
        for parent_id, children in by_parent.iteritems():
 
820
            if parent_id is ROOT_PARENT:
 
821
                continue
 
822
            if not self._any_contents(children):
 
823
                continue
 
824
            for child in children:
 
825
                try:
 
826
                    self.final_kind(child)
 
827
                except NoSuchFile:
 
828
                    continue
 
829
            try:
 
830
                kind = self.final_kind(parent_id)
 
831
            except NoSuchFile:
 
832
                kind = None
 
833
            if kind is None:
 
834
                conflicts.append(('missing parent', parent_id))
 
835
            elif kind != "directory":
 
836
                conflicts.append(('non-directory parent', parent_id))
 
837
        return conflicts
 
838
 
 
839
    def _any_contents(self, trans_ids):
 
840
        """Return true if any of the trans_ids, will have contents."""
 
841
        for trans_id in trans_ids:
 
842
            try:
 
843
                kind = self.final_kind(trans_id)
 
844
            except NoSuchFile:
 
845
                continue
 
846
            return True
 
847
        return False
 
848
 
 
849
    def _limbo_name(self, trans_id):
 
850
        """Generate the limbo name of a file"""
 
851
        limbo_name = self._limbo_files.get(trans_id)
 
852
        if limbo_name is not None:
 
853
            return limbo_name
 
854
        parent = self._new_parent.get(trans_id)
 
855
        # if the parent directory is already in limbo (e.g. when building a
 
856
        # tree), choose a limbo name inside the parent, to reduce further
 
857
        # renames.
 
858
        use_direct_path = False
 
859
        if self._new_contents.get(parent) == 'directory':
 
860
            filename = self._new_name.get(trans_id)
 
861
            if filename is not None:
 
862
                if parent not in self._limbo_children:
 
863
                    self._limbo_children[parent] = set()
 
864
                    self._limbo_children_names[parent] = {}
 
865
                    use_direct_path = True
 
866
                # the direct path can only be used if no other file has
 
867
                # already taken this pathname, i.e. if the name is unused, or
 
868
                # if it is already associated with this trans_id.
 
869
                elif self._case_sensitive_target:
 
870
                    if (self._limbo_children_names[parent].get(filename)
 
871
                        in (trans_id, None)):
 
872
                        use_direct_path = True
 
873
                else:
 
874
                    for l_filename, l_trans_id in\
 
875
                        self._limbo_children_names[parent].iteritems():
 
876
                        if l_trans_id == trans_id:
 
877
                            continue
 
878
                        if l_filename.lower() == filename.lower():
 
879
                            break
 
880
                    else:
 
881
                        use_direct_path = True
 
882
 
 
883
        if use_direct_path:
 
884
            limbo_name = pathjoin(self._limbo_files[parent], filename)
 
885
            self._limbo_children[parent].add(trans_id)
 
886
            self._limbo_children_names[parent][filename] = trans_id
 
887
        else:
 
888
            limbo_name = pathjoin(self._limbodir, trans_id)
 
889
            self._needs_rename.add(trans_id)
 
890
        self._limbo_files[trans_id] = limbo_name
 
891
        return limbo_name
 
892
 
 
893
    def _set_executability(self, path, trans_id):
 
894
        """Set the executability of versioned files """
 
895
        if supports_executable():
 
896
            new_executability = self._new_executability[trans_id]
 
897
            abspath = self._tree.abspath(path)
 
898
            current_mode = os.stat(abspath).st_mode
 
899
            if new_executability:
 
900
                umask = os.umask(0)
 
901
                os.umask(umask)
 
902
                to_mode = current_mode | (0100 & ~umask)
 
903
                # Enable x-bit for others only if they can read it.
 
904
                if current_mode & 0004:
 
905
                    to_mode |= 0001 & ~umask
 
906
                if current_mode & 0040:
 
907
                    to_mode |= 0010 & ~umask
 
908
            else:
 
909
                to_mode = current_mode & ~0111
 
910
            os.chmod(abspath, to_mode)
 
911
 
 
912
    def _new_entry(self, name, parent_id, file_id):
 
913
        """Helper function to create a new filesystem entry."""
 
914
        trans_id = self.create_path(name, parent_id)
 
915
        if file_id is not None:
 
916
            self.version_file(file_id, trans_id)
 
917
        return trans_id
 
918
 
 
919
    def new_file(self, name, parent_id, contents, file_id=None, 
 
920
                 executable=None):
 
921
        """Convenience method to create files.
 
922
        
 
923
        name is the name of the file to create.
 
924
        parent_id is the transaction id of the parent directory of the file.
 
925
        contents is an iterator of bytestrings, which will be used to produce
 
926
        the file.
 
927
        :param file_id: The inventory ID of the file, if it is to be versioned.
 
928
        :param executable: Only valid when a file_id has been supplied.
 
929
        """
 
930
        trans_id = self._new_entry(name, parent_id, file_id)
 
931
        # TODO: rather than scheduling a set_executable call,
 
932
        # have create_file create the file with the right mode.
 
933
        self.create_file(contents, trans_id)
 
934
        if executable is not None:
 
935
            self.set_executability(executable, trans_id)
 
936
        return trans_id
 
937
 
 
938
    def new_directory(self, name, parent_id, file_id=None):
 
939
        """Convenience method to create directories.
 
940
 
 
941
        name is the name of the directory to create.
 
942
        parent_id is the transaction id of the parent directory of the
 
943
        directory.
 
944
        file_id is the inventory ID of the directory, if it is to be versioned.
 
945
        """
 
946
        trans_id = self._new_entry(name, parent_id, file_id)
 
947
        self.create_directory(trans_id)
 
948
        return trans_id 
 
949
 
 
950
    def new_symlink(self, name, parent_id, target, file_id=None):
 
951
        """Convenience method to create symbolic link.
 
952
        
 
953
        name is the name of the symlink to create.
 
954
        parent_id is the transaction id of the parent directory of the symlink.
 
955
        target is a bytestring of the target of the symlink.
 
956
        file_id is the inventory ID of the file, if it is to be versioned.
 
957
        """
 
958
        trans_id = self._new_entry(name, parent_id, file_id)
 
959
        self.create_symlink(target, trans_id)
 
960
        return trans_id
 
961
 
 
962
    def _affected_ids(self):
 
963
        """Return the set of transform ids affected by the transform"""
 
964
        trans_ids = set(self._removed_id)
 
965
        trans_ids.update(self._new_id.keys())
 
966
        trans_ids.update(self._removed_contents)
 
967
        trans_ids.update(self._new_contents.keys())
 
968
        trans_ids.update(self._new_executability.keys())
 
969
        trans_ids.update(self._new_name.keys())
 
970
        trans_ids.update(self._new_parent.keys())
 
971
        return trans_ids
 
972
 
 
973
    def _get_file_id_maps(self):
 
974
        """Return mapping of file_ids to trans_ids in the to and from states"""
 
975
        trans_ids = self._affected_ids()
 
976
        from_trans_ids = {}
 
977
        to_trans_ids = {}
 
978
        # Build up two dicts: trans_ids associated with file ids in the
 
979
        # FROM state, vs the TO state.
 
980
        for trans_id in trans_ids:
 
981
            from_file_id = self.tree_file_id(trans_id)
 
982
            if from_file_id is not None:
 
983
                from_trans_ids[from_file_id] = trans_id
 
984
            to_file_id = self.final_file_id(trans_id)
 
985
            if to_file_id is not None:
 
986
                to_trans_ids[to_file_id] = trans_id
 
987
        return from_trans_ids, to_trans_ids
 
988
 
 
989
    def _from_file_data(self, from_trans_id, from_versioned, file_id):
 
990
        """Get data about a file in the from (tree) state
 
991
 
 
992
        Return a (name, parent, kind, executable) tuple
 
993
        """
 
994
        from_path = self._tree_id_paths.get(from_trans_id)
 
995
        if from_versioned:
 
996
            # get data from working tree if versioned
 
997
            from_entry = self._tree.inventory[file_id]
 
998
            from_name = from_entry.name
 
999
            from_parent = from_entry.parent_id
 
1000
        else:
 
1001
            from_entry = None
 
1002
            if from_path is None:
 
1003
                # File does not exist in FROM state
 
1004
                from_name = None
 
1005
                from_parent = None
 
1006
            else:
 
1007
                # File exists, but is not versioned.  Have to use path-
 
1008
                # splitting stuff
 
1009
                from_name = os.path.basename(from_path)
 
1010
                tree_parent = self.get_tree_parent(from_trans_id)
 
1011
                from_parent = self.tree_file_id(tree_parent)
 
1012
        if from_path is not None:
 
1013
            from_kind, from_executable, from_stats = \
 
1014
                self._tree._comparison_data(from_entry, from_path)
 
1015
        else:
 
1016
            from_kind = None
 
1017
            from_executable = False
 
1018
        return from_name, from_parent, from_kind, from_executable
 
1019
 
 
1020
    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
 
1021
        """Get data about a file in the to (target) state
 
1022
 
 
1023
        Return a (name, parent, kind, executable) tuple
 
1024
        """
 
1025
        to_name = self.final_name(to_trans_id)
 
1026
        try:
 
1027
            to_kind = self.final_kind(to_trans_id)
 
1028
        except NoSuchFile:
 
1029
            to_kind = None
 
1030
        to_parent = self.final_file_id(self.final_parent(to_trans_id))
 
1031
        if to_trans_id in self._new_executability:
 
1032
            to_executable = self._new_executability[to_trans_id]
 
1033
        elif to_trans_id == from_trans_id:
 
1034
            to_executable = from_executable
 
1035
        else:
 
1036
            to_executable = False
 
1037
        return to_name, to_parent, to_kind, to_executable
 
1038
 
 
1039
    def iter_changes(self):
 
1040
        """Produce output in the same format as Tree.iter_changes.
 
1041
 
 
1042
        Will produce nonsensical results if invoked while inventory/filesystem
 
1043
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
 
1044
 
 
1045
        This reads the Transform, but only reproduces changes involving a
 
1046
        file_id.  Files that are not versioned in either of the FROM or TO
 
1047
        states are not reflected.
 
1048
        """
 
1049
        final_paths = FinalPaths(self)
 
1050
        from_trans_ids, to_trans_ids = self._get_file_id_maps()
 
1051
        results = []
 
1052
        # Now iterate through all active file_ids
 
1053
        for file_id in set(from_trans_ids.keys() + to_trans_ids.keys()):
 
1054
            modified = False
 
1055
            from_trans_id = from_trans_ids.get(file_id)
 
1056
            # find file ids, and determine versioning state
 
1057
            if from_trans_id is None:
 
1058
                from_versioned = False
 
1059
                from_trans_id = to_trans_ids[file_id]
 
1060
            else:
 
1061
                from_versioned = True
 
1062
            to_trans_id = to_trans_ids.get(file_id)
 
1063
            if to_trans_id is None:
 
1064
                to_versioned = False
 
1065
                to_trans_id = from_trans_id
 
1066
            else:
 
1067
                to_versioned = True
 
1068
 
 
1069
            from_name, from_parent, from_kind, from_executable = \
 
1070
                self._from_file_data(from_trans_id, from_versioned, file_id)
 
1071
 
 
1072
            to_name, to_parent, to_kind, to_executable = \
 
1073
                self._to_file_data(to_trans_id, from_trans_id, from_executable)
 
1074
 
 
1075
            if not from_versioned:
 
1076
                from_path = None
 
1077
            else:
 
1078
                from_path = self._tree_id_paths.get(from_trans_id)
 
1079
            if not to_versioned:
 
1080
                to_path = None
 
1081
            else:
 
1082
                to_path = final_paths.get_path(to_trans_id)
 
1083
            if from_kind != to_kind:
 
1084
                modified = True
 
1085
            elif to_kind in ('file', 'symlink') and (
 
1086
                to_trans_id != from_trans_id or
 
1087
                to_trans_id in self._new_contents):
 
1088
                modified = True
 
1089
            if (not modified and from_versioned == to_versioned and
 
1090
                from_parent==to_parent and from_name == to_name and
 
1091
                from_executable == to_executable):
 
1092
                continue
 
1093
            results.append((file_id, (from_path, to_path), modified,
 
1094
                   (from_versioned, to_versioned),
 
1095
                   (from_parent, to_parent),
 
1096
                   (from_name, to_name),
 
1097
                   (from_kind, to_kind),
 
1098
                   (from_executable, to_executable)))
 
1099
        return iter(sorted(results, key=lambda x:x[1]))
 
1100
 
 
1101
    def get_preview_tree(self):
 
1102
        """Return a tree representing the result of the transform.
 
1103
 
 
1104
        This tree only supports the subset of Tree functionality required
 
1105
        by show_diff_trees.  It must only be compared to tt._tree.
 
1106
        """
 
1107
        return _PreviewTree(self)
 
1108
 
 
1109
 
 
1110
class TreeTransform(TreeTransformBase):
 
1111
    """Represent a tree transformation.
 
1112
 
 
1113
    This object is designed to support incremental generation of the transform,
 
1114
    in any order.
 
1115
 
 
1116
    However, it gives optimum performance when parent directories are created
 
1117
    before their contents.  The transform is then able to put child files
 
1118
    directly in their parent directory, avoiding later renames.
 
1119
 
 
1120
    It is easy to produce malformed transforms, but they are generally
 
1121
    harmless.  Attempting to apply a malformed transform will cause an
 
1122
    exception to be raised before any modifications are made to the tree.
 
1123
 
 
1124
    Many kinds of malformed transforms can be corrected with the
 
1125
    resolve_conflicts function.  The remaining ones indicate programming error,
 
1126
    such as trying to create a file with no path.
 
1127
 
 
1128
    Two sets of file creation methods are supplied.  Convenience methods are:
 
1129
     * new_file
 
1130
     * new_directory
 
1131
     * new_symlink
 
1132
 
 
1133
    These are composed of the low-level methods:
 
1134
     * create_path
 
1135
     * create_file or create_directory or create_symlink
 
1136
     * version_file
 
1137
     * set_executability
 
1138
 
 
1139
    Transform/Transaction ids
 
1140
    -------------------------
 
1141
    trans_ids are temporary ids assigned to all files involved in a transform.
 
1142
    It's possible, even common, that not all files in the Tree have trans_ids.
 
1143
 
 
1144
    trans_ids are used because filenames and file_ids are not good enough
 
1145
    identifiers; filenames change, and not all files have file_ids.  File-ids
 
1146
    are also associated with trans-ids, so that moving a file moves its
 
1147
    file-id.
 
1148
 
 
1149
    trans_ids are only valid for the TreeTransform that generated them.
 
1150
 
 
1151
    Limbo
 
1152
    -----
 
1153
    Limbo is a temporary directory use to hold new versions of files.
 
1154
    Files are added to limbo by create_file, create_directory, create_symlink,
 
1155
    and their convenience variants (new_*).  Files may be removed from limbo
 
1156
    using cancel_creation.  Files are renamed from limbo into their final
 
1157
    location as part of TreeTransform.apply
 
1158
 
 
1159
    Limbo must be cleaned up, by either calling TreeTransform.apply or
 
1160
    calling TreeTransform.finalize.
 
1161
 
 
1162
    Files are placed into limbo inside their parent directories, where
 
1163
    possible.  This reduces subsequent renames, and makes operations involving
 
1164
    lots of files faster.  This optimization is only possible if the parent
 
1165
    directory is created *before* creating any of its children, so avoid
 
1166
    creating children before parents, where possible.
 
1167
 
 
1168
    Pending-deletion
 
1169
    ----------------
 
1170
    This temporary directory is used by _FileMover for storing files that are
 
1171
    about to be deleted.  In case of rollback, the files will be restored.
 
1172
    FileMover does not delete files until it is sure that a rollback will not
 
1173
    happen.
 
1174
    """
 
1175
    def __init__(self, tree, pb=DummyProgress()):
 
1176
        """Note: a tree_write lock is taken on the tree.
 
1177
 
 
1178
        Use TreeTransform.finalize() to release the lock (can be omitted if
 
1179
        TreeTransform.apply() called).
 
1180
        """
 
1181
        tree.lock_tree_write()
 
1182
 
 
1183
        try:
 
1184
            limbodir = urlutils.local_path_from_url(
 
1185
                tree._transport.abspath('limbo'))
 
1186
            try:
 
1187
                os.mkdir(limbodir)
 
1188
            except OSError, e:
 
1189
                if e.errno == errno.EEXIST:
 
1190
                    raise ExistingLimbo(limbodir)
 
1191
            deletiondir = urlutils.local_path_from_url(
 
1192
                tree._transport.abspath('pending-deletion'))
 
1193
            try:
 
1194
                os.mkdir(deletiondir)
 
1195
            except OSError, e:
 
1196
                if e.errno == errno.EEXIST:
 
1197
                    raise errors.ExistingPendingDeletion(deletiondir)
 
1198
        except:
 
1199
            tree.unlock()
 
1200
            raise
 
1201
 
 
1202
        TreeTransformBase.__init__(self, tree, limbodir, pb,
 
1203
                                   tree.case_sensitive)
 
1204
        self._deletiondir = deletiondir
 
1205
 
 
1206
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
 
1207
        """Apply all changes to the inventory and filesystem.
 
1208
 
 
1209
        If filesystem or inventory conflicts are present, MalformedTransform
 
1210
        will be thrown.
 
1211
 
 
1212
        If apply succeeds, finalize is not necessary.
 
1213
 
 
1214
        :param no_conflicts: if True, the caller guarantees there are no
 
1215
            conflicts, so no check is made.
 
1216
        :param precomputed_delta: An inventory delta to use instead of
 
1217
            calculating one.
 
1218
        :param _mover: Supply an alternate FileMover, for testing
 
1219
        """
 
1220
        if not no_conflicts:
 
1221
            conflicts = self.find_conflicts()
 
1222
            if len(conflicts) != 0:
 
1223
                raise MalformedTransform(conflicts=conflicts)
 
1224
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1225
        try:
 
1226
            if precomputed_delta is None:
 
1227
                child_pb.update('Apply phase', 0, 2)
 
1228
                inventory_delta = self._generate_inventory_delta()
 
1229
                offset = 1
 
1230
            else:
 
1231
                inventory_delta = precomputed_delta
 
1232
                offset = 0
 
1233
            if _mover is None:
 
1234
                mover = _FileMover()
 
1235
            else:
 
1236
                mover = _mover
 
1237
            try:
 
1238
                child_pb.update('Apply phase', 0 + offset, 2 + offset)
 
1239
                self._apply_removals(mover)
 
1240
                child_pb.update('Apply phase', 1 + offset, 2 + offset)
 
1241
                modified_paths = self._apply_insertions(mover)
 
1242
            except:
 
1243
                mover.rollback()
 
1244
                raise
 
1245
            else:
 
1246
                mover.apply_deletions()
 
1247
        finally:
 
1248
            child_pb.finished()
 
1249
        self._tree.apply_inventory_delta(inventory_delta)
 
1250
        self._done = True
 
1251
        self.finalize()
 
1252
        return _TransformResults(modified_paths, self.rename_count)
 
1253
 
 
1254
    def _generate_inventory_delta(self):
 
1255
        """Generate an inventory delta for the current transform."""
 
1256
        inventory_delta = []
 
1257
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1258
        new_paths = self._inventory_altered()
 
1259
        total_entries = len(new_paths) + len(self._removed_id)
 
1260
        try:
 
1261
            for num, trans_id in enumerate(self._removed_id):
 
1262
                if (num % 10) == 0:
 
1263
                    child_pb.update('removing file', num, total_entries)
 
1264
                if trans_id == self._new_root:
 
1265
                    file_id = self._tree.get_root_id()
 
1266
                else:
 
1267
                    file_id = self.tree_file_id(trans_id)
 
1268
                # File-id isn't really being deleted, just moved
 
1269
                if file_id in self._r_new_id:
 
1270
                    continue
 
1271
                path = self._tree_id_paths[trans_id]
 
1272
                inventory_delta.append((path, None, file_id, None))
 
1273
            new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
 
1274
                                     new_paths)
 
1275
            entries = self._tree.iter_entries_by_dir(
 
1276
                new_path_file_ids.values())
 
1277
            old_paths = dict((e.file_id, p) for p, e in entries)
 
1278
            final_kinds = {}
 
1279
            for num, (path, trans_id) in enumerate(new_paths):
 
1280
                if (num % 10) == 0:
 
1281
                    child_pb.update('adding file',
 
1282
                                    num + len(self._removed_id), total_entries)
 
1283
                file_id = new_path_file_ids[trans_id]
 
1284
                if file_id is None:
 
1285
                    continue
 
1286
                needs_entry = False
 
1287
                try:
 
1288
                    kind = self.final_kind(trans_id)
 
1289
                except NoSuchFile:
 
1290
                    kind = self._tree.stored_kind(file_id)
 
1291
                parent_trans_id = self.final_parent(trans_id)
 
1292
                parent_file_id = new_path_file_ids.get(parent_trans_id)
 
1293
                if parent_file_id is None:
 
1294
                    parent_file_id = self.final_file_id(parent_trans_id)
 
1295
                if trans_id in self._new_reference_revision:
 
1296
                    new_entry = inventory.TreeReference(
 
1297
                        file_id,
 
1298
                        self._new_name[trans_id],
 
1299
                        self.final_file_id(self._new_parent[trans_id]),
 
1300
                        None, self._new_reference_revision[trans_id])
 
1301
                else:
 
1302
                    new_entry = inventory.make_entry(kind,
 
1303
                        self.final_name(trans_id),
 
1304
                        parent_file_id, file_id)
 
1305
                old_path = old_paths.get(new_entry.file_id)
 
1306
                new_executability = self._new_executability.get(trans_id)
 
1307
                if new_executability is not None:
 
1308
                    new_entry.executable = new_executability
 
1309
                inventory_delta.append(
 
1310
                    (old_path, path, new_entry.file_id, new_entry))
 
1311
        finally:
 
1312
            child_pb.finished()
 
1313
        return inventory_delta
 
1314
 
 
1315
    def _apply_removals(self, mover):
 
1316
        """Perform tree operations that remove directory/inventory names.
 
1317
 
 
1318
        That is, delete files that are to be deleted, and put any files that
 
1319
        need renaming into limbo.  This must be done in strict child-to-parent
 
1320
        order.
 
1321
 
 
1322
        If inventory_delta is None, no inventory delta generation is performed.
 
1323
        """
 
1324
        tree_paths = list(self._tree_path_ids.iteritems())
 
1325
        tree_paths.sort(reverse=True)
 
1326
        kind_changes = set()
 
1327
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1328
        try:
 
1329
            for num, data in enumerate(tree_paths):
 
1330
                path, trans_id = data
 
1331
                child_pb.update('removing file', num, len(tree_paths))
 
1332
                full_path = self._tree.abspath(path)
 
1333
                if trans_id in self._removed_contents:
 
1334
                    mover.pre_delete(full_path, os.path.join(self._deletiondir,
 
1335
                                     trans_id))
 
1336
                elif trans_id in self._new_name or trans_id in \
 
1337
                    self._new_parent:
 
1338
                    try:
 
1339
                        mover.rename(full_path, self._limbo_name(trans_id))
 
1340
                    except OSError, e:
 
1341
                        if e.errno != errno.ENOENT:
 
1342
                            raise
 
1343
                    else:
 
1344
                        self.rename_count += 1
 
1345
        finally:
 
1346
            child_pb.finished()
 
1347
        return kind_changes
 
1348
 
 
1349
    def _apply_insertions(self, mover):
 
1350
        """Perform tree operations that insert directory/inventory names.
 
1351
 
 
1352
        That is, create any files that need to be created, and restore from
 
1353
        limbo any files that needed renaming.  This must be done in strict
 
1354
        parent-to-child order.
 
1355
 
 
1356
        If inventory_delta is None, no inventory delta is calculated, and
 
1357
        no list of modified paths is returned.
 
1358
 
 
1359
        kind_changes is a set of trans ids where the entry has changed
 
1360
        kind, and so an inventory delta entry should be created for them.
 
1361
        """
 
1362
        new_paths = self.new_paths(filesystem_only=True)
 
1363
        modified_paths = []
 
1364
        new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
 
1365
                                 new_paths)
 
1366
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1367
        try:
 
1368
            for num, (path, trans_id) in enumerate(new_paths):
 
1369
                if (num % 10) == 0:
 
1370
                    child_pb.update('adding file', num, len(new_paths))
 
1371
                full_path = self._tree.abspath(path)
 
1372
                if trans_id in self._needs_rename:
 
1373
                    try:
 
1374
                        mover.rename(self._limbo_name(trans_id), full_path)
 
1375
                    except OSError, e:
 
1376
                        # We may be renaming a dangling inventory id
 
1377
                        if e.errno != errno.ENOENT:
 
1378
                            raise
 
1379
                    else:
 
1380
                        self.rename_count += 1
 
1381
                if (trans_id in self._new_contents or
 
1382
                    self.path_changed(trans_id)):
 
1383
                    if trans_id in self._new_contents:
 
1384
                        modified_paths.append(full_path)
 
1385
                if trans_id in self._new_executability:
 
1386
                    self._set_executability(path, trans_id)
 
1387
        finally:
 
1388
            child_pb.finished()
 
1389
        self._new_contents.clear()
 
1390
        return modified_paths
 
1391
 
 
1392
 
 
1393
class TransformPreview(TreeTransformBase):
 
1394
    """A TreeTransform for generating preview trees.
 
1395
 
 
1396
    Unlike TreeTransform, this version works when the input tree is a
 
1397
    RevisionTree, rather than a WorkingTree.  As a result, it tends to ignore
 
1398
    unversioned files in the input tree.
 
1399
    """
 
1400
 
 
1401
    def __init__(self, tree, pb=DummyProgress(), case_sensitive=True):
 
1402
        tree.lock_read()
 
1403
        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
 
1404
        TreeTransformBase.__init__(self, tree, limbodir, pb, case_sensitive)
 
1405
 
 
1406
    def canonical_path(self, path):
 
1407
        return path
 
1408
 
 
1409
    def tree_kind(self, trans_id):
 
1410
        path = self._tree_id_paths.get(trans_id)
 
1411
        if path is None:
 
1412
            raise NoSuchFile(None)
 
1413
        file_id = self._tree.path2id(path)
 
1414
        return self._tree.kind(file_id)
 
1415
 
 
1416
    def _set_mode(self, trans_id, mode_id, typefunc):
 
1417
        """Set the mode of new file contents.
 
1418
        The mode_id is the existing file to get the mode from (often the same
 
1419
        as trans_id).  The operation is only performed if there's a mode match
 
1420
        according to typefunc.
 
1421
        """
 
1422
        # is it ok to ignore this?  probably
 
1423
        pass
 
1424
 
 
1425
    def iter_tree_children(self, parent_id):
 
1426
        """Iterate through the entry's tree children, if any"""
 
1427
        try:
 
1428
            path = self._tree_id_paths[parent_id]
 
1429
        except KeyError:
 
1430
            return
 
1431
        file_id = self.tree_file_id(parent_id)
 
1432
        if file_id is None:
 
1433
            return
 
1434
        children = getattr(self._tree.inventory[file_id], 'children', {})
 
1435
        for child in children:
 
1436
            childpath = joinpath(path, child)
 
1437
            yield self.trans_id_tree_path(childpath)
 
1438
 
 
1439
 
 
1440
class _PreviewTree(tree.Tree):
 
1441
    """Partial implementation of Tree to support show_diff_trees"""
 
1442
 
 
1443
    def __init__(self, transform):
 
1444
        self._transform = transform
 
1445
        self._final_paths = FinalPaths(transform)
 
1446
        self.__by_parent = None
 
1447
        self._parent_ids = []
 
1448
 
 
1449
    def _changes(self, file_id):
 
1450
        for changes in self._transform.iter_changes():
 
1451
            if changes[0] == file_id:
 
1452
                return changes
 
1453
 
 
1454
    def _content_change(self, file_id):
 
1455
        """Return True if the content of this file changed"""
 
1456
        changes = self._changes(file_id)
 
1457
        # changes[2] is true if the file content changed.  See
 
1458
        # InterTree.iter_changes.
 
1459
        return (changes is not None and changes[2])
 
1460
 
 
1461
    def _get_repository(self):
 
1462
        repo = getattr(self._transform._tree, '_repository', None)
 
1463
        if repo is None:
 
1464
            repo = self._transform._tree.branch.repository
 
1465
        return repo
 
1466
 
 
1467
    def _iter_parent_trees(self):
 
1468
        for revision_id in self.get_parent_ids():
 
1469
            try:
 
1470
                yield self.revision_tree(revision_id)
 
1471
            except errors.NoSuchRevisionInTree:
 
1472
                yield self._get_repository().revision_tree(revision_id)
 
1473
 
 
1474
    def _get_file_revision(self, file_id, vf, tree_revision):
 
1475
        parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
 
1476
                       self._iter_parent_trees()]
 
1477
        vf.add_lines((file_id, tree_revision), parent_keys,
 
1478
                     self.get_file(file_id).readlines())
 
1479
        repo = self._get_repository()
 
1480
        base_vf = repo.texts
 
1481
        if base_vf not in vf.fallback_versionedfiles:
 
1482
            vf.fallback_versionedfiles.append(base_vf)
 
1483
        return tree_revision
 
1484
 
 
1485
    def _stat_limbo_file(self, file_id):
 
1486
        trans_id = self._transform.trans_id_file_id(file_id)
 
1487
        name = self._transform._limbo_name(trans_id)
 
1488
        return os.lstat(name)
 
1489
 
 
1490
    @property
 
1491
    def _by_parent(self):
 
1492
        if self.__by_parent is None:
 
1493
            self.__by_parent = self._transform.by_parent()
 
1494
        return self.__by_parent
 
1495
 
 
1496
    def lock_read(self):
 
1497
        # Perhaps in theory, this should lock the TreeTransform?
 
1498
        pass
 
1499
 
 
1500
    def unlock(self):
 
1501
        pass
 
1502
 
 
1503
    @property
 
1504
    def inventory(self):
 
1505
        """This Tree does not use inventory as its backing data."""
 
1506
        raise NotImplementedError(_PreviewTree.inventory)
 
1507
 
 
1508
    def get_root_id(self):
 
1509
        return self._transform.final_file_id(self._transform.root)
 
1510
 
 
1511
    def all_file_ids(self):
 
1512
        tree_ids = set(self._transform._tree.all_file_ids())
 
1513
        tree_ids.difference_update(self._transform.tree_file_id(t)
 
1514
                                   for t in self._transform._removed_id)
 
1515
        tree_ids.update(self._transform._new_id.values())
 
1516
        return tree_ids
 
1517
 
 
1518
    def __iter__(self):
 
1519
        return iter(self.all_file_ids())
 
1520
 
 
1521
    def paths2ids(self, specific_files, trees=None, require_versioned=False):
 
1522
        """See Tree.paths2ids"""
 
1523
        to_find = set(specific_files)
 
1524
        result = set()
 
1525
        for (file_id, paths, changed, versioned, parent, name, kind,
 
1526
             executable) in self._transform.iter_changes():
 
1527
            if paths[1] in to_find:
 
1528
                result.append(file_id)
 
1529
                to_find.remove(paths[1])
 
1530
        result.update(self._transform._tree.paths2ids(to_find,
 
1531
                      trees=[], require_versioned=require_versioned))
 
1532
        return result
 
1533
 
 
1534
    def _path2trans_id(self, path):
 
1535
        segments = splitpath(path)
 
1536
        cur_parent = self._transform.root
 
1537
        for cur_segment in segments:
 
1538
            for child in self._all_children(cur_parent):
 
1539
                if self._transform.final_name(child) == cur_segment:
 
1540
                    cur_parent = child
 
1541
                    break
 
1542
            else:
 
1543
                return None
 
1544
        return cur_parent
 
1545
 
 
1546
    def path2id(self, path):
 
1547
        return self._transform.final_file_id(self._path2trans_id(path))
 
1548
 
 
1549
    def id2path(self, file_id):
 
1550
        trans_id = self._transform.trans_id_file_id(file_id)
 
1551
        try:
 
1552
            return self._final_paths._determine_path(trans_id)
 
1553
        except NoFinalPath:
 
1554
            raise errors.NoSuchId(self, file_id)
 
1555
 
 
1556
    def _all_children(self, trans_id):
 
1557
        children = set(self._transform.iter_tree_children(trans_id))
 
1558
        # children in the _new_parent set are provided by _by_parent.
 
1559
        children.difference_update(self._transform._new_parent.keys())
 
1560
        children.update(self._by_parent.get(trans_id, []))
 
1561
        return children
 
1562
 
 
1563
    def _make_inv_entries(self, ordered_entries, specific_file_ids):
 
1564
        for trans_id, parent_file_id in ordered_entries:
 
1565
            file_id = self._transform.final_file_id(trans_id)
 
1566
            if file_id is None:
 
1567
                continue
 
1568
            if (specific_file_ids is not None
 
1569
                and file_id not in specific_file_ids):
 
1570
                continue
 
1571
            try:
 
1572
                kind = self._transform.final_kind(trans_id)
 
1573
            except NoSuchFile:
 
1574
                kind = self._transform._tree.stored_kind(file_id)
 
1575
            new_entry = inventory.make_entry(
 
1576
                kind,
 
1577
                self._transform.final_name(trans_id),
 
1578
                parent_file_id, file_id)
 
1579
            yield new_entry, trans_id
 
1580
 
 
1581
    def iter_entries_by_dir(self, specific_file_ids=None):
 
1582
        # This may not be a maximally efficient implementation, but it is
 
1583
        # reasonably straightforward.  An implementation that grafts the
 
1584
        # TreeTransform changes onto the tree's iter_entries_by_dir results
 
1585
        # might be more efficient, but requires tricky inferences about stack
 
1586
        # position.
 
1587
        todo = [ROOT_PARENT]
 
1588
        ordered_ids = []
 
1589
        while len(todo) > 0:
 
1590
            parent = todo.pop()
 
1591
            parent_file_id = self._transform.final_file_id(parent)
 
1592
            children = list(self._all_children(parent))
 
1593
            paths = dict(zip(children, self._final_paths.get_paths(children)))
 
1594
            children.sort(key=paths.get)
 
1595
            todo.extend(reversed(children))
 
1596
            for trans_id in children:
 
1597
                ordered_ids.append((trans_id, parent_file_id))
 
1598
        for entry, trans_id in self._make_inv_entries(ordered_ids,
 
1599
                                                      specific_file_ids):
 
1600
            yield unicode(self._final_paths.get_path(trans_id)), entry
 
1601
 
 
1602
    def kind(self, file_id):
 
1603
        trans_id = self._transform.trans_id_file_id(file_id)
 
1604
        return self._transform.final_kind(trans_id)
 
1605
 
 
1606
    def stored_kind(self, file_id):
 
1607
        trans_id = self._transform.trans_id_file_id(file_id)
 
1608
        try:
 
1609
            return self._transform._new_contents[trans_id]
 
1610
        except KeyError:
 
1611
            return self._transform._tree.stored_kind(file_id)
 
1612
 
 
1613
    def get_file_mtime(self, file_id, path=None):
 
1614
        """See Tree.get_file_mtime"""
 
1615
        if not self._content_change(file_id):
 
1616
            return self._transform._tree.get_file_mtime(file_id, path)
 
1617
        return self._stat_limbo_file(file_id).st_mtime
 
1618
 
 
1619
    def get_file_size(self, file_id):
 
1620
        """See Tree.get_file_size"""
 
1621
        if self.kind(file_id) == 'file':
 
1622
            return self._transform._tree.get_file_size(file_id)
 
1623
        else:
 
1624
            return None
 
1625
 
 
1626
    def get_file_sha1(self, file_id, path=None, stat_value=None):
 
1627
        return self._transform._tree.get_file_sha1(file_id)
 
1628
 
 
1629
    def is_executable(self, file_id, path=None):
 
1630
        trans_id = self._transform.trans_id_file_id(file_id)
 
1631
        try:
 
1632
            return self._transform._new_executability[trans_id]
 
1633
        except KeyError:
 
1634
            return self._transform._tree.is_executable(file_id, path)
 
1635
 
 
1636
    def path_content_summary(self, path):
 
1637
        trans_id = self._path2trans_id(path)
 
1638
        tt = self._transform
 
1639
        tree_path = tt._tree_id_paths.get(trans_id)
 
1640
        kind = tt._new_contents.get(trans_id)
 
1641
        if kind is None:
 
1642
            if tree_path is None or trans_id in tt._removed_contents:
 
1643
                return 'missing', None, None, None
 
1644
            summary = tt._tree.path_content_summary(tree_path)
 
1645
            kind, size, executable, link_or_sha1 = summary
 
1646
        else:
 
1647
            link_or_sha1 = None
 
1648
            limbo_name = tt._limbo_name(trans_id)
 
1649
            if trans_id in tt._new_reference_revision:
 
1650
                kind = 'tree-reference'
 
1651
            if kind == 'file':
 
1652
                statval = os.lstat(limbo_name)
 
1653
                size = statval.st_size
 
1654
                if not supports_executable():
 
1655
                    executable = None
 
1656
                else:
 
1657
                    executable = statval.st_mode & S_IEXEC
 
1658
            else:
 
1659
                size = None
 
1660
                executable = None
 
1661
            if kind == 'symlink':
 
1662
                link_or_sha1 = os.readlink(limbo_name)
 
1663
        if supports_executable():
 
1664
            executable = tt._new_executability.get(trans_id, executable)
 
1665
        return kind, size, executable, link_or_sha1
 
1666
 
 
1667
    def iter_changes(self, from_tree, include_unchanged=False,
 
1668
                      specific_files=None, pb=None, extra_trees=None,
 
1669
                      require_versioned=True, want_unversioned=False):
 
1670
        """See InterTree.iter_changes.
 
1671
 
 
1672
        This implementation does not support include_unchanged, specific_files,
 
1673
        or want_unversioned.  extra_trees, require_versioned, and pb are
 
1674
        ignored.
 
1675
        """
 
1676
        if from_tree is not self._transform._tree:
 
1677
            raise ValueError('from_tree must be transform source tree.')
 
1678
        if include_unchanged:
 
1679
            raise ValueError('include_unchanged is not supported')
 
1680
        if specific_files is not None:
 
1681
            raise ValueError('specific_files is not supported')
 
1682
        if want_unversioned:
 
1683
            raise ValueError('want_unversioned is not supported')
 
1684
        return self._transform.iter_changes()
 
1685
 
 
1686
    def get_file(self, file_id, path=None):
 
1687
        """See Tree.get_file"""
 
1688
        if not self._content_change(file_id):
 
1689
            return self._transform._tree.get_file(file_id, path)
 
1690
        trans_id = self._transform.trans_id_file_id(file_id)
 
1691
        name = self._transform._limbo_name(trans_id)
 
1692
        return open(name, 'rb')
 
1693
 
 
1694
    def get_file_text(self, file_id):
 
1695
        text_file = self.get_file(file_id)
 
1696
        try:
 
1697
            return text_file.read()
 
1698
        finally:
 
1699
            text_file.close()
 
1700
 
 
1701
    def annotate_iter(self, file_id,
 
1702
                      default_revision=_mod_revision.CURRENT_REVISION):
 
1703
        changes = self._changes(file_id)
 
1704
        if changes is None:
 
1705
            get_old = True
 
1706
        else:
 
1707
            changed_content, versioned, kind = (changes[2], changes[3],
 
1708
                                                changes[6])
 
1709
            if kind[1] is None:
 
1710
                return None
 
1711
            get_old = (kind[0] == 'file' and versioned[0])
 
1712
        if get_old:
 
1713
            old_annotation = self._transform._tree.annotate_iter(file_id,
 
1714
                default_revision=default_revision)
 
1715
        else:
 
1716
            old_annotation = []
 
1717
        if changes is None:
 
1718
            return old_annotation
 
1719
        if not changed_content:
 
1720
            return old_annotation
 
1721
        return annotate.reannotate([old_annotation],
 
1722
                                   self.get_file(file_id).readlines(),
 
1723
                                   default_revision)
 
1724
 
 
1725
    def get_symlink_target(self, file_id):
 
1726
        """See Tree.get_symlink_target"""
 
1727
        if not self._content_change(file_id):
 
1728
            return self._transform._tree.get_symlink_target(file_id)
 
1729
        trans_id = self._transform.trans_id_file_id(file_id)
 
1730
        name = self._transform._limbo_name(trans_id)
 
1731
        return os.readlink(name)
 
1732
 
 
1733
    def list_files(self, include_root=False):
 
1734
        return self._transform._tree.list_files(include_root)
 
1735
 
 
1736
    def walkdirs(self, prefix=""):
 
1737
        return self._transform._tree.walkdirs(prefix)
 
1738
 
 
1739
    def get_parent_ids(self):
 
1740
        return self._parent_ids
 
1741
 
 
1742
    def set_parent_ids(self, parent_ids):
 
1743
        self._parent_ids = parent_ids
 
1744
 
 
1745
    def get_revision_tree(self, revision_id):
 
1746
        return self._transform._tree.get_revision_tree(revision_id)
 
1747
 
 
1748
 
 
1749
def joinpath(parent, child):
 
1750
    """Join tree-relative paths, handling the tree root specially"""
 
1751
    if parent is None or parent == "":
 
1752
        return child
 
1753
    else:
 
1754
        return pathjoin(parent, child)
 
1755
 
 
1756
 
 
1757
class FinalPaths(object):
 
1758
    """Make path calculation cheap by memoizing paths.
 
1759
 
 
1760
    The underlying tree must not be manipulated between calls, or else
 
1761
    the results will likely be incorrect.
 
1762
    """
 
1763
    def __init__(self, transform):
 
1764
        object.__init__(self)
 
1765
        self._known_paths = {}
 
1766
        self.transform = transform
 
1767
 
 
1768
    def _determine_path(self, trans_id):
 
1769
        if trans_id == self.transform.root:
 
1770
            return ""
 
1771
        name = self.transform.final_name(trans_id)
 
1772
        parent_id = self.transform.final_parent(trans_id)
 
1773
        if parent_id == self.transform.root:
 
1774
            return name
 
1775
        else:
 
1776
            return pathjoin(self.get_path(parent_id), name)
 
1777
 
 
1778
    def get_path(self, trans_id):
 
1779
        """Find the final path associated with a trans_id"""
 
1780
        if trans_id not in self._known_paths:
 
1781
            self._known_paths[trans_id] = self._determine_path(trans_id)
 
1782
        return self._known_paths[trans_id]
 
1783
 
 
1784
    def get_paths(self, trans_ids):
 
1785
        return [(self.get_path(t), t) for t in trans_ids]
 
1786
 
 
1787
 
 
1788
 
 
1789
def topology_sorted_ids(tree):
 
1790
    """Determine the topological order of the ids in a tree"""
 
1791
    file_ids = list(tree)
 
1792
    file_ids.sort(key=tree.id2path)
 
1793
    return file_ids
 
1794
 
 
1795
 
 
1796
def build_tree(tree, wt, accelerator_tree=None, hardlink=False,
 
1797
               delta_from_tree=False):
 
1798
    """Create working tree for a branch, using a TreeTransform.
 
1799
    
 
1800
    This function should be used on empty trees, having a tree root at most.
 
1801
    (see merge and revert functionality for working with existing trees)
 
1802
 
 
1803
    Existing files are handled like so:
 
1804
    
 
1805
    - Existing bzrdirs take precedence over creating new items.  They are
 
1806
      created as '%s.diverted' % name.
 
1807
    - Otherwise, if the content on disk matches the content we are building,
 
1808
      it is silently replaced.
 
1809
    - Otherwise, conflict resolution will move the old file to 'oldname.moved'.
 
1810
 
 
1811
    :param tree: The tree to convert wt into a copy of
 
1812
    :param wt: The working tree that files will be placed into
 
1813
    :param accelerator_tree: A tree which can be used for retrieving file
 
1814
        contents more quickly than tree itself, i.e. a workingtree.  tree
 
1815
        will be used for cases where accelerator_tree's content is different.
 
1816
    :param hardlink: If true, hard-link files to accelerator_tree, where
 
1817
        possible.  accelerator_tree must implement abspath, i.e. be a
 
1818
        working tree.
 
1819
    :param delta_from_tree: If true, build_tree may use the input Tree to
 
1820
        generate the inventory delta.
 
1821
    """
 
1822
    wt.lock_tree_write()
 
1823
    try:
 
1824
        tree.lock_read()
 
1825
        try:
 
1826
            if accelerator_tree is not None:
 
1827
                accelerator_tree.lock_read()
 
1828
            try:
 
1829
                return _build_tree(tree, wt, accelerator_tree, hardlink,
 
1830
                                   delta_from_tree)
 
1831
            finally:
 
1832
                if accelerator_tree is not None:
 
1833
                    accelerator_tree.unlock()
 
1834
        finally:
 
1835
            tree.unlock()
 
1836
    finally:
 
1837
        wt.unlock()
 
1838
 
 
1839
 
 
1840
def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree):
 
1841
    """See build_tree."""
 
1842
    for num, _unused in enumerate(wt.all_file_ids()):
 
1843
        if num > 0:  # more than just a root
 
1844
            raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir)
 
1845
    existing_files = set()
 
1846
    for dir, files in wt.walkdirs():
 
1847
        existing_files.update(f[0] for f in files)
 
1848
    file_trans_id = {}
 
1849
    top_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1850
    pp = ProgressPhase("Build phase", 2, top_pb)
 
1851
    if tree.inventory.root is not None:
 
1852
        # This is kind of a hack: we should be altering the root
 
1853
        # as part of the regular tree shape diff logic.
 
1854
        # The conditional test here is to avoid doing an
 
1855
        # expensive operation (flush) every time the root id
 
1856
        # is set within the tree, nor setting the root and thus
 
1857
        # marking the tree as dirty, because we use two different
 
1858
        # idioms here: tree interfaces and inventory interfaces.
 
1859
        if wt.get_root_id() != tree.get_root_id():
 
1860
            wt.set_root_id(tree.get_root_id())
 
1861
            wt.flush()
 
1862
    tt = TreeTransform(wt)
 
1863
    divert = set()
 
1864
    try:
 
1865
        pp.next_phase()
 
1866
        file_trans_id[wt.get_root_id()] = \
 
1867
            tt.trans_id_tree_file_id(wt.get_root_id())
 
1868
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
1869
        try:
 
1870
            deferred_contents = []
 
1871
            num = 0
 
1872
            total = len(tree.inventory)
 
1873
            if delta_from_tree:
 
1874
                precomputed_delta = []
 
1875
            else:
 
1876
                precomputed_delta = None
 
1877
            for num, (tree_path, entry) in \
 
1878
                enumerate(tree.inventory.iter_entries_by_dir()):
 
1879
                pb.update("Building tree", num - len(deferred_contents), total)
 
1880
                if entry.parent_id is None:
 
1881
                    continue
 
1882
                reparent = False
 
1883
                file_id = entry.file_id
 
1884
                if delta_from_tree:
 
1885
                    precomputed_delta.append((None, tree_path, file_id, entry))
 
1886
                if tree_path in existing_files:
 
1887
                    target_path = wt.abspath(tree_path)
 
1888
                    kind = file_kind(target_path)
 
1889
                    if kind == "directory":
 
1890
                        try:
 
1891
                            bzrdir.BzrDir.open(target_path)
 
1892
                        except errors.NotBranchError:
 
1893
                            pass
 
1894
                        else:
 
1895
                            divert.add(file_id)
 
1896
                    if (file_id not in divert and
 
1897
                        _content_match(tree, entry, file_id, kind,
 
1898
                        target_path)):
 
1899
                        tt.delete_contents(tt.trans_id_tree_path(tree_path))
 
1900
                        if kind == 'directory':
 
1901
                            reparent = True
 
1902
                parent_id = file_trans_id[entry.parent_id]
 
1903
                if entry.kind == 'file':
 
1904
                    # We *almost* replicate new_by_entry, so that we can defer
 
1905
                    # getting the file text, and get them all at once.
 
1906
                    trans_id = tt.create_path(entry.name, parent_id)
 
1907
                    file_trans_id[file_id] = trans_id
 
1908
                    tt.version_file(file_id, trans_id)
 
1909
                    executable = tree.is_executable(file_id, tree_path)
 
1910
                    if executable:
 
1911
                        tt.set_executability(executable, trans_id)
 
1912
                    deferred_contents.append((file_id, trans_id))
 
1913
                else:
 
1914
                    file_trans_id[file_id] = new_by_entry(tt, entry, parent_id,
 
1915
                                                          tree)
 
1916
                if reparent:
 
1917
                    new_trans_id = file_trans_id[file_id]
 
1918
                    old_parent = tt.trans_id_tree_path(tree_path)
 
1919
                    _reparent_children(tt, old_parent, new_trans_id)
 
1920
            offset = num + 1 - len(deferred_contents)
 
1921
            _create_files(tt, tree, deferred_contents, pb, offset,
 
1922
                          accelerator_tree, hardlink)
 
1923
        finally:
 
1924
            pb.finished()
 
1925
        pp.next_phase()
 
1926
        divert_trans = set(file_trans_id[f] for f in divert)
 
1927
        resolver = lambda t, c: resolve_checkout(t, c, divert_trans)
 
1928
        raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
 
1929
        if len(raw_conflicts) > 0:
 
1930
            precomputed_delta = None
 
1931
        conflicts = cook_conflicts(raw_conflicts, tt)
 
1932
        for conflict in conflicts:
 
1933
            warning(conflict)
 
1934
        try:
 
1935
            wt.add_conflicts(conflicts)
 
1936
        except errors.UnsupportedOperation:
 
1937
            pass
 
1938
        result = tt.apply(no_conflicts=True,
 
1939
                          precomputed_delta=precomputed_delta)
 
1940
    finally:
 
1941
        tt.finalize()
 
1942
        top_pb.finished()
 
1943
    return result
 
1944
 
 
1945
 
 
1946
def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree,
 
1947
                  hardlink):
 
1948
    total = len(desired_files) + offset
 
1949
    if accelerator_tree is None:
 
1950
        new_desired_files = desired_files
 
1951
    else:
 
1952
        iter = accelerator_tree.iter_changes(tree, include_unchanged=True)
 
1953
        unchanged = dict((f, p[1]) for (f, p, c, v, d, n, k, e)
 
1954
                         in iter if not (c or e[0] != e[1]))
 
1955
        new_desired_files = []
 
1956
        count = 0
 
1957
        for file_id, trans_id in desired_files:
 
1958
            accelerator_path = unchanged.get(file_id)
 
1959
            if accelerator_path is None:
 
1960
                new_desired_files.append((file_id, trans_id))
 
1961
                continue
 
1962
            pb.update('Adding file contents', count + offset, total)
 
1963
            if hardlink:
 
1964
                tt.create_hardlink(accelerator_tree.abspath(accelerator_path),
 
1965
                                   trans_id)
 
1966
            else:
 
1967
                contents = accelerator_tree.get_file(file_id, accelerator_path)
 
1968
                try:
 
1969
                    tt.create_file(contents, trans_id)
 
1970
                finally:
 
1971
                    contents.close()
 
1972
            count += 1
 
1973
        offset += count
 
1974
    for count, (trans_id, contents) in enumerate(tree.iter_files_bytes(
 
1975
                                                 new_desired_files)):
 
1976
        tt.create_file(contents, trans_id)
 
1977
        pb.update('Adding file contents', count + offset, total)
 
1978
 
 
1979
 
 
1980
def _reparent_children(tt, old_parent, new_parent):
 
1981
    for child in tt.iter_tree_children(old_parent):
 
1982
        tt.adjust_path(tt.final_name(child), new_parent, child)
 
1983
 
 
1984
def _reparent_transform_children(tt, old_parent, new_parent):
 
1985
    by_parent = tt.by_parent()
 
1986
    for child in by_parent[old_parent]:
 
1987
        tt.adjust_path(tt.final_name(child), new_parent, child)
 
1988
    return by_parent[old_parent]
 
1989
 
 
1990
def _content_match(tree, entry, file_id, kind, target_path):
 
1991
    if entry.kind != kind:
 
1992
        return False
 
1993
    if entry.kind == "directory":
 
1994
        return True
 
1995
    if entry.kind == "file":
 
1996
        if tree.get_file(file_id).read() == file(target_path, 'rb').read():
 
1997
            return True
 
1998
    elif entry.kind == "symlink":
 
1999
        if tree.get_symlink_target(file_id) == os.readlink(target_path):
 
2000
            return True
 
2001
    return False
 
2002
 
 
2003
 
 
2004
def resolve_checkout(tt, conflicts, divert):
 
2005
    new_conflicts = set()
 
2006
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
2007
        # Anything but a 'duplicate' would indicate programmer error
 
2008
        if c_type != 'duplicate':
 
2009
            raise AssertionError(c_type)
 
2010
        # Now figure out which is new and which is old
 
2011
        if tt.new_contents(conflict[1]):
 
2012
            new_file = conflict[1]
 
2013
            old_file = conflict[2]
 
2014
        else:
 
2015
            new_file = conflict[2]
 
2016
            old_file = conflict[1]
 
2017
 
 
2018
        # We should only get here if the conflict wasn't completely
 
2019
        # resolved
 
2020
        final_parent = tt.final_parent(old_file)
 
2021
        if new_file in divert:
 
2022
            new_name = tt.final_name(old_file)+'.diverted'
 
2023
            tt.adjust_path(new_name, final_parent, new_file)
 
2024
            new_conflicts.add((c_type, 'Diverted to',
 
2025
                               new_file, old_file))
 
2026
        else:
 
2027
            new_name = tt.final_name(old_file)+'.moved'
 
2028
            tt.adjust_path(new_name, final_parent, old_file)
 
2029
            new_conflicts.add((c_type, 'Moved existing file to',
 
2030
                               old_file, new_file))
 
2031
    return new_conflicts
 
2032
 
 
2033
 
 
2034
def new_by_entry(tt, entry, parent_id, tree):
 
2035
    """Create a new file according to its inventory entry"""
 
2036
    name = entry.name
 
2037
    kind = entry.kind
 
2038
    if kind == 'file':
 
2039
        contents = tree.get_file(entry.file_id).readlines()
 
2040
        executable = tree.is_executable(entry.file_id)
 
2041
        return tt.new_file(name, parent_id, contents, entry.file_id, 
 
2042
                           executable)
 
2043
    elif kind in ('directory', 'tree-reference'):
 
2044
        trans_id = tt.new_directory(name, parent_id, entry.file_id)
 
2045
        if kind == 'tree-reference':
 
2046
            tt.set_tree_reference(entry.reference_revision, trans_id)
 
2047
        return trans_id 
 
2048
    elif kind == 'symlink':
 
2049
        target = tree.get_symlink_target(entry.file_id)
 
2050
        return tt.new_symlink(name, parent_id, target, entry.file_id)
 
2051
    else:
 
2052
        raise errors.BadFileKindError(name, kind)
 
2053
 
 
2054
 
 
2055
def create_by_entry(tt, entry, tree, trans_id, lines=None, mode_id=None):
 
2056
    """Create new file contents according to an inventory entry."""
 
2057
    if entry.kind == "file":
 
2058
        if lines is None:
 
2059
            lines = tree.get_file(entry.file_id).readlines()
 
2060
        tt.create_file(lines, trans_id, mode_id=mode_id)
 
2061
    elif entry.kind == "symlink":
 
2062
        tt.create_symlink(tree.get_symlink_target(entry.file_id), trans_id)
 
2063
    elif entry.kind == "directory":
 
2064
        tt.create_directory(trans_id)
 
2065
 
 
2066
 
 
2067
def create_entry_executability(tt, entry, trans_id):
 
2068
    """Set the executability of a trans_id according to an inventory entry"""
 
2069
    if entry.kind == "file":
 
2070
        tt.set_executability(entry.executable, trans_id)
 
2071
 
 
2072
 
 
2073
def get_backup_name(entry, by_parent, parent_trans_id, tt):
 
2074
    return _get_backup_name(entry.name, by_parent, parent_trans_id, tt)
 
2075
 
 
2076
 
 
2077
def _get_backup_name(name, by_parent, parent_trans_id, tt):
 
2078
    """Produce a backup-style name that appears to be available"""
 
2079
    def name_gen():
 
2080
        counter = 1
 
2081
        while True:
 
2082
            yield "%s.~%d~" % (name, counter)
 
2083
            counter += 1
 
2084
    for new_name in name_gen():
 
2085
        if not tt.has_named_child(by_parent, parent_trans_id, new_name):
 
2086
            return new_name
 
2087
 
 
2088
 
 
2089
def _entry_changes(file_id, entry, working_tree):
 
2090
    """Determine in which ways the inventory entry has changed.
 
2091
 
 
2092
    Returns booleans: has_contents, content_mod, meta_mod
 
2093
    has_contents means there are currently contents, but they differ
 
2094
    contents_mod means contents need to be modified
 
2095
    meta_mod means the metadata needs to be modified
 
2096
    """
 
2097
    cur_entry = working_tree.inventory[file_id]
 
2098
    try:
 
2099
        working_kind = working_tree.kind(file_id)
 
2100
        has_contents = True
 
2101
    except NoSuchFile:
 
2102
        has_contents = False
 
2103
        contents_mod = True
 
2104
        meta_mod = False
 
2105
    if has_contents is True:
 
2106
        if entry.kind != working_kind:
 
2107
            contents_mod, meta_mod = True, False
 
2108
        else:
 
2109
            cur_entry._read_tree_state(working_tree.id2path(file_id), 
 
2110
                                       working_tree)
 
2111
            contents_mod, meta_mod = entry.detect_changes(cur_entry)
 
2112
            cur_entry._forget_tree_state()
 
2113
    return has_contents, contents_mod, meta_mod
 
2114
 
 
2115
 
 
2116
def revert(working_tree, target_tree, filenames, backups=False,
 
2117
           pb=DummyProgress(), change_reporter=None):
 
2118
    """Revert a working tree's contents to those of a target tree."""
 
2119
    target_tree.lock_read()
 
2120
    tt = TreeTransform(working_tree, pb)
 
2121
    try:
 
2122
        pp = ProgressPhase("Revert phase", 3, pb)
 
2123
        pp.next_phase()
 
2124
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
2125
        try:
 
2126
            merge_modified = _alter_files(working_tree, target_tree, tt,
 
2127
                                          child_pb, filenames, backups)
 
2128
        finally:
 
2129
            child_pb.finished()
 
2130
        pp.next_phase()
 
2131
        child_pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
2132
        try:
 
2133
            raw_conflicts = resolve_conflicts(tt, child_pb,
 
2134
                lambda t, c: conflict_pass(t, c, target_tree))
 
2135
        finally:
 
2136
            child_pb.finished()
 
2137
        conflicts = cook_conflicts(raw_conflicts, tt)
 
2138
        if change_reporter:
 
2139
            change_reporter = delta._ChangeReporter(
 
2140
                unversioned_filter=working_tree.is_ignored)
 
2141
            delta.report_changes(tt.iter_changes(), change_reporter)
 
2142
        for conflict in conflicts:
 
2143
            warning(conflict)
 
2144
        pp.next_phase()
 
2145
        tt.apply()
 
2146
        working_tree.set_merge_modified(merge_modified)
 
2147
    finally:
 
2148
        target_tree.unlock()
 
2149
        tt.finalize()
 
2150
        pb.clear()
 
2151
    return conflicts
 
2152
 
 
2153
 
 
2154
def _alter_files(working_tree, target_tree, tt, pb, specific_files,
 
2155
                 backups):
 
2156
    merge_modified = working_tree.merge_modified()
 
2157
    change_list = target_tree.iter_changes(working_tree,
 
2158
        specific_files=specific_files, pb=pb)
 
2159
    if target_tree.inventory.root is None:
 
2160
        skip_root = True
 
2161
    else:
 
2162
        skip_root = False
 
2163
    basis_tree = None
 
2164
    try:
 
2165
        deferred_files = []
 
2166
        for id_num, (file_id, path, changed_content, versioned, parent, name,
 
2167
                kind, executable) in enumerate(change_list):
 
2168
            if skip_root and file_id[0] is not None and parent[0] is None:
 
2169
                continue
 
2170
            trans_id = tt.trans_id_file_id(file_id)
 
2171
            mode_id = None
 
2172
            if changed_content:
 
2173
                keep_content = False
 
2174
                if kind[0] == 'file' and (backups or kind[1] is None):
 
2175
                    wt_sha1 = working_tree.get_file_sha1(file_id)
 
2176
                    if merge_modified.get(file_id) != wt_sha1:
 
2177
                        # acquire the basis tree lazily to prevent the
 
2178
                        # expense of accessing it when it's not needed ?
 
2179
                        # (Guessing, RBC, 200702)
 
2180
                        if basis_tree is None:
 
2181
                            basis_tree = working_tree.basis_tree()
 
2182
                            basis_tree.lock_read()
 
2183
                        if file_id in basis_tree:
 
2184
                            if wt_sha1 != basis_tree.get_file_sha1(file_id):
 
2185
                                keep_content = True
 
2186
                        elif kind[1] is None and not versioned[1]:
 
2187
                            keep_content = True
 
2188
                if kind[0] is not None:
 
2189
                    if not keep_content:
 
2190
                        tt.delete_contents(trans_id)
 
2191
                    elif kind[1] is not None:
 
2192
                        parent_trans_id = tt.trans_id_file_id(parent[0])
 
2193
                        by_parent = tt.by_parent()
 
2194
                        backup_name = _get_backup_name(name[0], by_parent,
 
2195
                                                       parent_trans_id, tt)
 
2196
                        tt.adjust_path(backup_name, parent_trans_id, trans_id)
 
2197
                        new_trans_id = tt.create_path(name[0], parent_trans_id)
 
2198
                        if versioned == (True, True):
 
2199
                            tt.unversion_file(trans_id)
 
2200
                            tt.version_file(file_id, new_trans_id)
 
2201
                        # New contents should have the same unix perms as old
 
2202
                        # contents
 
2203
                        mode_id = trans_id
 
2204
                        trans_id = new_trans_id
 
2205
                if kind[1] == 'directory':
 
2206
                    tt.create_directory(trans_id)
 
2207
                elif kind[1] == 'symlink':
 
2208
                    tt.create_symlink(target_tree.get_symlink_target(file_id),
 
2209
                                      trans_id)
 
2210
                elif kind[1] == 'file':
 
2211
                    deferred_files.append((file_id, (trans_id, mode_id)))
 
2212
                    if basis_tree is None:
 
2213
                        basis_tree = working_tree.basis_tree()
 
2214
                        basis_tree.lock_read()
 
2215
                    new_sha1 = target_tree.get_file_sha1(file_id)
 
2216
                    if (file_id in basis_tree and new_sha1 ==
 
2217
                        basis_tree.get_file_sha1(file_id)):
 
2218
                        if file_id in merge_modified:
 
2219
                            del merge_modified[file_id]
 
2220
                    else:
 
2221
                        merge_modified[file_id] = new_sha1
 
2222
 
 
2223
                    # preserve the execute bit when backing up
 
2224
                    if keep_content and executable[0] == executable[1]:
 
2225
                        tt.set_executability(executable[1], trans_id)
 
2226
                elif kind[1] is not None:
 
2227
                    raise AssertionError(kind[1])
 
2228
            if versioned == (False, True):
 
2229
                tt.version_file(file_id, trans_id)
 
2230
            if versioned == (True, False):
 
2231
                tt.unversion_file(trans_id)
 
2232
            if (name[1] is not None and 
 
2233
                (name[0] != name[1] or parent[0] != parent[1])):
 
2234
                tt.adjust_path(
 
2235
                    name[1], tt.trans_id_file_id(parent[1]), trans_id)
 
2236
            if executable[0] != executable[1] and kind[1] == "file":
 
2237
                tt.set_executability(executable[1], trans_id)
 
2238
        for (trans_id, mode_id), bytes in target_tree.iter_files_bytes(
 
2239
            deferred_files):
 
2240
            tt.create_file(bytes, trans_id, mode_id)
 
2241
    finally:
 
2242
        if basis_tree is not None:
 
2243
            basis_tree.unlock()
 
2244
    return merge_modified
 
2245
 
 
2246
 
 
2247
def resolve_conflicts(tt, pb=DummyProgress(), pass_func=None):
 
2248
    """Make many conflict-resolution attempts, but die if they fail"""
 
2249
    if pass_func is None:
 
2250
        pass_func = conflict_pass
 
2251
    new_conflicts = set()
 
2252
    try:
 
2253
        for n in range(10):
 
2254
            pb.update('Resolution pass', n+1, 10)
 
2255
            conflicts = tt.find_conflicts()
 
2256
            if len(conflicts) == 0:
 
2257
                return new_conflicts
 
2258
            new_conflicts.update(pass_func(tt, conflicts))
 
2259
        raise MalformedTransform(conflicts=conflicts)
 
2260
    finally:
 
2261
        pb.clear()
 
2262
 
 
2263
 
 
2264
def conflict_pass(tt, conflicts, path_tree=None):
 
2265
    """Resolve some classes of conflicts.
 
2266
 
 
2267
    :param tt: The transform to resolve conflicts in
 
2268
    :param conflicts: The conflicts to resolve
 
2269
    :param path_tree: A Tree to get supplemental paths from
 
2270
    """
 
2271
    new_conflicts = set()
 
2272
    for c_type, conflict in ((c[0], c) for c in conflicts):
 
2273
        if c_type == 'duplicate id':
 
2274
            tt.unversion_file(conflict[1])
 
2275
            new_conflicts.add((c_type, 'Unversioned existing file',
 
2276
                               conflict[1], conflict[2], ))
 
2277
        elif c_type == 'duplicate':
 
2278
            # files that were renamed take precedence
 
2279
            final_parent = tt.final_parent(conflict[1])
 
2280
            if tt.path_changed(conflict[1]):
 
2281
                existing_file, new_file = conflict[2], conflict[1]
 
2282
            else:
 
2283
                existing_file, new_file = conflict[1], conflict[2]
 
2284
            new_name = tt.final_name(existing_file)+'.moved'
 
2285
            tt.adjust_path(new_name, final_parent, existing_file)
 
2286
            new_conflicts.add((c_type, 'Moved existing file to', 
 
2287
                               existing_file, new_file))
 
2288
        elif c_type == 'parent loop':
 
2289
            # break the loop by undoing one of the ops that caused the loop
 
2290
            cur = conflict[1]
 
2291
            while not tt.path_changed(cur):
 
2292
                cur = tt.final_parent(cur)
 
2293
            new_conflicts.add((c_type, 'Cancelled move', cur,
 
2294
                               tt.final_parent(cur),))
 
2295
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
2296
            
 
2297
        elif c_type == 'missing parent':
 
2298
            trans_id = conflict[1]
 
2299
            try:
 
2300
                tt.cancel_deletion(trans_id)
 
2301
                new_conflicts.add(('deleting parent', 'Not deleting', 
 
2302
                                   trans_id))
 
2303
            except KeyError:
 
2304
                create = True
 
2305
                try:
 
2306
                    tt.final_name(trans_id)
 
2307
                except NoFinalPath:
 
2308
                    if path_tree is not None:
 
2309
                        file_id = tt.final_file_id(trans_id)
 
2310
                        if file_id is None:
 
2311
                            file_id = tt.inactive_file_id(trans_id)
 
2312
                        entry = path_tree.inventory[file_id]
 
2313
                        # special-case the other tree root (move its
 
2314
                        # children to current root)
 
2315
                        if entry.parent_id is None:
 
2316
                            create=False
 
2317
                            moved = _reparent_transform_children(
 
2318
                                tt, trans_id, tt.root)
 
2319
                            for child in moved:
 
2320
                                new_conflicts.add((c_type, 'Moved to root',
 
2321
                                                   child))
 
2322
                        else:
 
2323
                            parent_trans_id = tt.trans_id_file_id(
 
2324
                                entry.parent_id)
 
2325
                            tt.adjust_path(entry.name, parent_trans_id,
 
2326
                                           trans_id)
 
2327
                if create:
 
2328
                    tt.create_directory(trans_id)
 
2329
                    new_conflicts.add((c_type, 'Created directory', trans_id))
 
2330
        elif c_type == 'unversioned parent':
 
2331
            file_id = tt.inactive_file_id(conflict[1])
 
2332
            # special-case the other tree root (move its children instead)
 
2333
            if path_tree and file_id in path_tree:
 
2334
                if path_tree.inventory[file_id].parent_id is None:
 
2335
                    continue
 
2336
            tt.version_file(file_id, conflict[1])
 
2337
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
 
2338
        elif c_type == 'non-directory parent':
 
2339
            parent_id = conflict[1]
 
2340
            parent_parent = tt.final_parent(parent_id)
 
2341
            parent_name = tt.final_name(parent_id)
 
2342
            parent_file_id = tt.final_file_id(parent_id)
 
2343
            new_parent_id = tt.new_directory(parent_name + '.new',
 
2344
                parent_parent, parent_file_id)
 
2345
            _reparent_transform_children(tt, parent_id, new_parent_id)
 
2346
            if parent_file_id is not None:
 
2347
                tt.unversion_file(parent_id)
 
2348
            new_conflicts.add((c_type, 'Created directory', new_parent_id))
 
2349
    return new_conflicts
 
2350
 
 
2351
 
 
2352
def cook_conflicts(raw_conflicts, tt):
 
2353
    """Generate a list of cooked conflicts, sorted by file path"""
 
2354
    from bzrlib.conflicts import Conflict
 
2355
    conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
 
2356
    return sorted(conflict_iter, key=Conflict.sort_key)
 
2357
 
 
2358
 
 
2359
def iter_cook_conflicts(raw_conflicts, tt):
 
2360
    from bzrlib.conflicts import Conflict
 
2361
    fp = FinalPaths(tt)
 
2362
    for conflict in raw_conflicts:
 
2363
        c_type = conflict[0]
 
2364
        action = conflict[1]
 
2365
        modified_path = fp.get_path(conflict[2])
 
2366
        modified_id = tt.final_file_id(conflict[2])
 
2367
        if len(conflict) == 3:
 
2368
            yield Conflict.factory(c_type, action=action, path=modified_path,
 
2369
                                     file_id=modified_id)
 
2370
             
 
2371
        else:
 
2372
            conflicting_path = fp.get_path(conflict[3])
 
2373
            conflicting_id = tt.final_file_id(conflict[3])
 
2374
            yield Conflict.factory(c_type, action=action, path=modified_path,
 
2375
                                   file_id=modified_id, 
 
2376
                                   conflict_path=conflicting_path,
 
2377
                                   conflict_file_id=conflicting_id)
 
2378
 
 
2379
 
 
2380
class _FileMover(object):
 
2381
    """Moves and deletes files for TreeTransform, tracking operations"""
 
2382
 
 
2383
    def __init__(self):
 
2384
        self.past_renames = []
 
2385
        self.pending_deletions = []
 
2386
 
 
2387
    def rename(self, from_, to):
 
2388
        """Rename a file from one path to another.  Functions like os.rename"""
 
2389
        try:
 
2390
            os.rename(from_, to)
 
2391
        except OSError, e:
 
2392
            if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
 
2393
                raise errors.FileExists(to, str(e))
 
2394
            raise
 
2395
        self.past_renames.append((from_, to))
 
2396
 
 
2397
    def pre_delete(self, from_, to):
 
2398
        """Rename a file out of the way and mark it for deletion.
 
2399
 
 
2400
        Unlike os.unlink, this works equally well for files and directories.
 
2401
        :param from_: The current file path
 
2402
        :param to: A temporary path for the file
 
2403
        """
 
2404
        self.rename(from_, to)
 
2405
        self.pending_deletions.append(to)
 
2406
 
 
2407
    def rollback(self):
 
2408
        """Reverse all renames that have been performed"""
 
2409
        for from_, to in reversed(self.past_renames):
 
2410
            os.rename(to, from_)
 
2411
        # after rollback, don't reuse _FileMover
 
2412
        past_renames = None
 
2413
        pending_deletions = None
 
2414
 
 
2415
    def apply_deletions(self):
 
2416
        """Apply all marked deletions"""
 
2417
        for path in self.pending_deletions:
 
2418
            delete_any(path)
 
2419
        # after apply_deletions, don't reuse _FileMover
 
2420
        past_renames = None
 
2421
        pending_deletions = None