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

  • Committer: Jelmer Vernooij
  • Date: 2020-08-10 15:00:17 UTC
  • mfrom: (7490.40.99 work)
  • mto: This revision was merged to the branch mainline in revision 7521.
  • Revision ID: jelmer@jelmer.uk-20200810150017-vs7xnrd1vat4iktg
Merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
from __future__ import absolute_import
18
 
 
 
17
import contextlib
19
18
import os
20
19
import errno
21
20
from stat import S_ISREG, S_IEXEC
26
25
    controldir,
27
26
    errors,
28
27
    lazy_import,
 
28
    lock,
29
29
    osutils,
30
30
    registry,
31
31
    trace,
32
 
    tree,
33
32
    )
34
33
lazy_import.lazy_import(globals(), """
35
34
from breezy import (
36
 
    annotate,
37
 
    cleanup,
38
 
    commit,
39
 
    conflicts,
40
 
    lock,
41
35
    multiparent,
42
36
    revision as _mod_revision,
43
37
    ui,
44
38
    urlutils,
45
39
    )
46
40
from breezy.i18n import gettext
47
 
from breezy.bzr import (
48
 
    inventory,
49
 
    inventorytree,
50
 
    )
51
41
""")
52
42
 
53
43
from .errors import (DuplicateKey,
54
 
                     CantMoveRoot,
55
44
                     BzrError, InternalBzrError)
56
45
from .filters import filtered_output_bytes, ContentFilterContext
57
46
from .mutabletree import MutableTree
64
53
    supports_symlinks,
65
54
    )
66
55
from .progress import ProgressPhase
67
 
from .sixish import (
68
 
    text_type,
69
 
    viewitems,
70
 
    viewvalues,
71
 
    )
72
56
from .tree import (
73
57
    InterTree,
74
 
    TreeChange,
 
58
    find_previous_path,
75
59
    )
76
60
 
77
61
 
100
84
    _fmt = "Tree transform is malformed %(conflicts)r"
101
85
 
102
86
 
 
87
class CantMoveRoot(BzrError):
 
88
 
 
89
    _fmt = "Moving the root directory is not supported at this time"
 
90
 
 
91
 
103
92
class ImmortalLimbo(BzrError):
104
93
 
105
94
    _fmt = """Unable to delete transform temporary directory %(limbo_dir)s.
111
100
        self.limbo_dir = limbo_dir
112
101
 
113
102
 
 
103
class TransformRenameFailed(BzrError):
 
104
 
 
105
    _fmt = "Failed to rename %(from_path)s to %(to_path)s: %(why)s"
 
106
 
 
107
    def __init__(self, from_path, to_path, why, errno):
 
108
        self.from_path = from_path
 
109
        self.to_path = to_path
 
110
        self.why = why
 
111
        self.errno = errno
 
112
 
 
113
 
114
114
def unique_add(map, key, value):
115
115
    if key in map:
116
116
        raise DuplicateKey(key=key)
118
118
 
119
119
 
120
120
class _TransformResults(object):
 
121
 
121
122
    def __init__(self, modified_paths, rename_count):
122
123
        object.__init__(self)
123
124
        self.modified_paths = modified_paths
124
125
        self.rename_count = rename_count
125
126
 
126
127
 
127
 
class TreeTransformBase(object):
128
 
    """The base class for TreeTransform and its kin."""
129
 
 
130
 
    def __init__(self, tree, pb=None, case_sensitive=True):
131
 
        """Constructor.
132
 
 
133
 
        :param tree: The tree that will be transformed, but not necessarily
134
 
            the output tree.
135
 
        :param pb: ignored
136
 
        :param case_sensitive: If True, the target of the transform is
137
 
            case sensitive, not just case preserving.
138
 
        """
139
 
        object.__init__(self)
 
128
class TreeTransform(object):
 
129
    """Represent a tree transformation.
 
130
 
 
131
    This object is designed to support incremental generation of the transform,
 
132
    in any order.
 
133
 
 
134
    However, it gives optimum performance when parent directories are created
 
135
    before their contents.  The transform is then able to put child files
 
136
    directly in their parent directory, avoiding later renames.
 
137
 
 
138
    It is easy to produce malformed transforms, but they are generally
 
139
    harmless.  Attempting to apply a malformed transform will cause an
 
140
    exception to be raised before any modifications are made to the tree.
 
141
 
 
142
    Many kinds of malformed transforms can be corrected with the
 
143
    resolve_conflicts function.  The remaining ones indicate programming error,
 
144
    such as trying to create a file with no path.
 
145
 
 
146
    Two sets of file creation methods are supplied.  Convenience methods are:
 
147
     * new_file
 
148
     * new_directory
 
149
     * new_symlink
 
150
 
 
151
    These are composed of the low-level methods:
 
152
     * create_path
 
153
     * create_file or create_directory or create_symlink
 
154
     * version_file
 
155
     * set_executability
 
156
 
 
157
    Transform/Transaction ids
 
158
    -------------------------
 
159
    trans_ids are temporary ids assigned to all files involved in a transform.
 
160
    It's possible, even common, that not all files in the Tree have trans_ids.
 
161
 
 
162
    trans_ids are only valid for the TreeTransform that generated them.
 
163
    """
 
164
 
 
165
    def __init__(self, tree, pb=None):
140
166
        self._tree = tree
 
167
        # A progress bar
 
168
        self._pb = pb
141
169
        self._id_number = 0
 
170
        # Mapping of path in old tree -> trans_id
 
171
        self._tree_path_ids = {}
 
172
        # Mapping trans_id -> path in old tree
 
173
        self._tree_id_paths = {}
142
174
        # mapping of trans_id -> new basename
143
175
        self._new_name = {}
144
176
        # mapping of trans_id -> new parent trans_id
145
177
        self._new_parent = {}
146
178
        # mapping of trans_id with new contents -> new file_kind
147
179
        self._new_contents = {}
148
 
        # mapping of trans_id => (sha1 of content, stat_value)
149
 
        self._observed_sha1s = {}
150
180
        # Set of trans_ids whose contents will be removed
151
181
        self._removed_contents = set()
152
182
        # Mapping of trans_id -> new execute-bit value
153
183
        self._new_executability = {}
154
184
        # Mapping of trans_id -> new tree-reference value
155
185
        self._new_reference_revision = {}
156
 
        # Mapping of trans_id -> new file_id
157
 
        self._new_id = {}
158
 
        # Mapping of old file-id -> trans_id
159
 
        self._non_present_ids = {}
160
 
        # Mapping of new file_id -> trans_id
161
 
        self._r_new_id = {}
162
186
        # Set of trans_ids that will be removed
163
187
        self._removed_id = set()
164
 
        # Mapping of path in old tree -> trans_id
165
 
        self._tree_path_ids = {}
166
 
        # Mapping trans_id -> path in old tree
167
 
        self._tree_id_paths = {}
168
 
        # The trans_id that will be used as the tree root
169
 
        if tree.is_versioned(''):
170
 
            self._new_root = self.trans_id_tree_path('')
171
 
        else:
172
 
            self._new_root = None
173
188
        # Indicator of whether the transform has been applied
174
189
        self._done = False
175
 
        # A progress bar
176
 
        self._pb = pb
177
 
        # Whether the target is case sensitive
178
 
        self._case_sensitive_target = case_sensitive
179
 
        # A counter of how many files have been renamed
180
 
        self.rename_count = 0
181
190
 
182
191
    def __enter__(self):
183
192
        """Support Context Manager API."""
195
204
        """
196
205
        raise NotImplementedError(self.iter_tree_children)
197
206
 
198
 
    def _read_symlink_target(self, trans_id):
199
 
        raise NotImplementedError(self._read_symlink_target)
200
 
 
201
207
    def canonical_path(self, path):
202
208
        return path
203
209
 
204
210
    def tree_kind(self, trans_id):
205
211
        raise NotImplementedError(self.tree_kind)
206
212
 
 
213
    def by_parent(self):
 
214
        """Return a map of parent: children for known parents.
 
215
 
 
216
        Only new paths and parents of tree files with assigned ids are used.
 
217
        """
 
218
        by_parent = {}
 
219
        items = list(self._new_parent.items())
 
220
        items.extend((t, self.final_parent(t))
 
221
                     for t in list(self._tree_id_paths))
 
222
        for trans_id, parent_id in items:
 
223
            if parent_id not in by_parent:
 
224
                by_parent[parent_id] = set()
 
225
            by_parent[parent_id].add(trans_id)
 
226
        return by_parent
 
227
 
207
228
    def finalize(self):
208
229
        """Release the working tree lock, if held.
209
230
 
210
231
        This is required if apply has not been invoked, but can be invoked
211
232
        even after apply.
212
233
        """
213
 
        if self._tree is None:
214
 
            return
215
 
        for hook in MutableTree.hooks['post_transform']:
216
 
            hook(self._tree, self)
217
 
        self._tree.unlock()
218
 
        self._tree = None
219
 
 
220
 
    def __get_root(self):
221
 
        return self._new_root
222
 
 
223
 
    root = property(__get_root)
224
 
 
225
 
    def _assign_id(self):
226
 
        """Produce a new tranform id"""
227
 
        new_id = "new-%s" % self._id_number
228
 
        self._id_number += 1
229
 
        return new_id
 
234
        raise NotImplementedError(self.finalize)
230
235
 
231
236
    def create_path(self, name, parent):
232
237
        """Assign a transaction id to a new path"""
253
258
        physically new directory, and hope someone versions the tree root
254
259
        later.
255
260
        """
256
 
        old_root = self._new_root
257
 
        old_root_file_id = self.final_file_id(old_root)
258
 
        # force moving all children of root
259
 
        for child_id in self.iter_tree_children(old_root):
260
 
            if child_id != parent:
261
 
                self.adjust_path(self.final_name(child_id),
262
 
                                 self.final_parent(child_id), child_id)
263
 
            file_id = self.final_file_id(child_id)
264
 
            if file_id is not None:
265
 
                self.unversion_file(child_id)
266
 
            self.version_file(child_id, file_id=file_id)
267
 
 
268
 
        # the physical root needs a new transaction id
269
 
        self._tree_path_ids.pop("")
270
 
        self._tree_id_paths.pop(old_root)
271
 
        self._new_root = self.trans_id_tree_path('')
272
 
        if parent == old_root:
273
 
            parent = self._new_root
274
 
        self.adjust_path(name, parent, old_root)
275
 
        self.create_directory(old_root)
276
 
        self.version_file(old_root, file_id=old_root_file_id)
277
 
        self.unversion_file(self._new_root)
 
261
        raise NotImplementedError(self.adjust_root_path)
278
262
 
279
263
    def fixup_new_roots(self):
280
264
        """Reinterpret requests to change the root directory
286
270
        This means that the old root trans-id becomes obsolete, so it is
287
271
        recommended only to invoke this after the root trans-id has become
288
272
        irrelevant.
289
 
 
290
 
        """
291
 
        new_roots = [k for k, v in viewitems(self._new_parent)
292
 
                     if v == ROOT_PARENT]
293
 
        if len(new_roots) < 1:
294
 
            return
295
 
        if len(new_roots) != 1:
296
 
            raise ValueError('A tree cannot have two roots!')
297
 
        if self._new_root is None:
298
 
            self._new_root = new_roots[0]
299
 
            return
300
 
        old_new_root = new_roots[0]
301
 
        # unversion the new root's directory.
302
 
        if self.final_kind(self._new_root) is None:
303
 
            file_id = self.final_file_id(old_new_root)
304
 
        else:
305
 
            file_id = self.final_file_id(self._new_root)
306
 
        if old_new_root in self._new_id:
307
 
            self.cancel_versioning(old_new_root)
308
 
        else:
309
 
            self.unversion_file(old_new_root)
310
 
        # if, at this stage, root still has an old file_id, zap it so we can
311
 
        # stick a new one in.
312
 
        if (self.tree_file_id(self._new_root) is not None
313
 
                and self._new_root not in self._removed_id):
314
 
            self.unversion_file(self._new_root)
315
 
        if file_id is not None:
316
 
            self.version_file(self._new_root, file_id=file_id)
317
 
 
318
 
        # Now move children of new root into old root directory.
319
 
        # Ensure all children are registered with the transaction, but don't
320
 
        # use directly-- some tree children have new parents
321
 
        list(self.iter_tree_children(old_new_root))
322
 
        # Move all children of new root into old root directory.
323
 
        for child in self.by_parent().get(old_new_root, []):
324
 
            self.adjust_path(self.final_name(child), self._new_root, child)
325
 
 
326
 
        # Ensure old_new_root has no directory.
327
 
        if old_new_root in self._new_contents:
328
 
            self.cancel_creation(old_new_root)
329
 
        else:
330
 
            self.delete_contents(old_new_root)
331
 
 
332
 
        # prevent deletion of root directory.
333
 
        if self._new_root in self._removed_contents:
334
 
            self.cancel_deletion(self._new_root)
335
 
 
336
 
        # destroy path info for old_new_root.
337
 
        del self._new_parent[old_new_root]
338
 
        del self._new_name[old_new_root]
339
 
 
340
 
    def trans_id_file_id(self, file_id):
341
 
        """Determine or set the transaction id associated with a file ID.
342
 
        A new id is only created for file_ids that were never present.  If
343
 
        a transaction has been unversioned, it is deliberately still returned.
344
 
        (this will likely lead to an unversioned parent conflict.)
345
 
        """
346
 
        if file_id is None:
347
 
            raise ValueError('None is not a valid file id')
348
 
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
349
 
            return self._r_new_id[file_id]
350
 
        else:
351
 
            try:
352
 
                path = self._tree.id2path(file_id)
353
 
            except errors.NoSuchId:
354
 
                if file_id in self._non_present_ids:
355
 
                    return self._non_present_ids[file_id]
356
 
                else:
357
 
                    trans_id = self._assign_id()
358
 
                    self._non_present_ids[file_id] = trans_id
359
 
                    return trans_id
360
 
            else:
361
 
                return self.trans_id_tree_path(path)
 
273
        """
 
274
        raise NotImplementedError(self.fixup_new_roots)
 
275
 
 
276
    def _assign_id(self):
 
277
        """Produce a new tranform id"""
 
278
        new_id = "new-%s" % self._id_number
 
279
        self._id_number += 1
 
280
        return new_id
362
281
 
363
282
    def trans_id_tree_path(self, path):
364
283
        """Determine (and maybe set) the transaction ID for a tree path."""
385
304
        """Cancel a scheduled deletion"""
386
305
        self._removed_contents.remove(trans_id)
387
306
 
388
 
    def unversion_file(self, trans_id):
389
 
        """Schedule a path entry to become unversioned"""
390
 
        self._removed_id.add(trans_id)
391
 
 
392
307
    def delete_versioned(self, trans_id):
393
308
        """Delete and unversion a versioned file"""
394
309
        self.delete_contents(trans_id)
409
324
 
410
325
    def version_file(self, trans_id, file_id=None):
411
326
        """Schedule a file to become versioned."""
412
 
        if file_id is None:
413
 
            raise ValueError()
414
 
        unique_add(self._new_id, trans_id, file_id)
415
 
        unique_add(self._r_new_id, file_id, trans_id)
 
327
        raise NotImplementedError(self.version_file)
416
328
 
417
329
    def cancel_versioning(self, trans_id):
418
330
        """Undo a previous versioning of a file"""
419
 
        file_id = self._new_id[trans_id]
420
 
        del self._new_id[trans_id]
421
 
        del self._r_new_id[file_id]
 
331
        raise NotImplementedError(self.cancel_versioning)
 
332
 
 
333
    def unversion_file(self, trans_id):
 
334
        """Schedule a path entry to become unversioned"""
 
335
        self._removed_id.add(trans_id)
422
336
 
423
337
    def new_paths(self, filesystem_only=False):
424
338
        """Determine the paths of all new and changed files.
426
340
        :param filesystem_only: if True, only calculate values for files
427
341
            that require renames or execute bit changes.
428
342
        """
429
 
        new_ids = set()
430
 
        if filesystem_only:
431
 
            stale_ids = self._needs_rename.difference(self._new_name)
432
 
            stale_ids.difference_update(self._new_parent)
433
 
            stale_ids.difference_update(self._new_contents)
434
 
            stale_ids.difference_update(self._new_id)
435
 
            needs_rename = self._needs_rename.difference(stale_ids)
436
 
            id_sets = (needs_rename, self._new_executability)
437
 
        else:
438
 
            id_sets = (self._new_name, self._new_parent, self._new_contents,
439
 
                       self._new_id, self._new_executability)
440
 
        for id_set in id_sets:
441
 
            new_ids.update(id_set)
442
 
        return sorted(FinalPaths(self).get_paths(new_ids))
443
 
 
444
 
    def _inventory_altered(self):
445
 
        """Determine which trans_ids need new Inventory entries.
446
 
 
447
 
        An new entry is needed when anything that would be reflected by an
448
 
        inventory entry changes, including file name, file_id, parent file_id,
449
 
        file kind, and the execute bit.
450
 
 
451
 
        Some care is taken to return entries with real changes, not cases
452
 
        where the value is deleted and then restored to its original value,
453
 
        but some actually unchanged values may be returned.
454
 
 
455
 
        :returns: A list of (path, trans_id) for all items requiring an
456
 
            inventory change. Ordered by path.
457
 
        """
458
 
        changed_ids = set()
459
 
        # Find entries whose file_ids are new (or changed).
460
 
        new_file_id = set(t for t in self._new_id
461
 
                          if self._new_id[t] != self.tree_file_id(t))
462
 
        for id_set in [self._new_name, self._new_parent, new_file_id,
463
 
                       self._new_executability]:
464
 
            changed_ids.update(id_set)
465
 
        # removing implies a kind change
466
 
        changed_kind = set(self._removed_contents)
467
 
        # so does adding
468
 
        changed_kind.intersection_update(self._new_contents)
469
 
        # Ignore entries that are already known to have changed.
470
 
        changed_kind.difference_update(changed_ids)
471
 
        #  to keep only the truly changed ones
472
 
        changed_kind = (t for t in changed_kind
473
 
                        if self.tree_kind(t) != self.final_kind(t))
474
 
        # all kind changes will alter the inventory
475
 
        changed_ids.update(changed_kind)
476
 
        # To find entries with changed parent_ids, find parents which existed,
477
 
        # but changed file_id.
478
 
        # Now add all their children to the set.
479
 
        for parent_trans_id in new_file_id:
480
 
            changed_ids.update(self.iter_tree_children(parent_trans_id))
481
 
        return sorted(FinalPaths(self).get_paths(changed_ids))
 
343
        raise NotImplementedError(self.new_paths)
482
344
 
483
345
    def final_kind(self, trans_id):
484
346
        """Determine the final file kind, after any changes applied.
498
360
        """Determine the tree path associated with the trans_id."""
499
361
        return self._tree_id_paths.get(trans_id)
500
362
 
501
 
    def tree_file_id(self, trans_id):
502
 
        """Determine the file id associated with the trans_id in the tree"""
503
 
        path = self.tree_path(trans_id)
504
 
        if path is None:
505
 
            return None
506
 
        # the file is old; the old id is still valid
507
 
        if self._new_root == trans_id:
508
 
            return self._tree.path2id('')
509
 
        return self._tree.path2id(path)
510
 
 
511
 
    def final_file_id(self, trans_id):
512
 
        """Determine the file id after any changes are applied, or None.
513
 
 
514
 
        None indicates that the file will not be versioned after changes are
515
 
        applied.
516
 
        """
517
 
        try:
518
 
            return self._new_id[trans_id]
519
 
        except KeyError:
520
 
            if trans_id in self._removed_id:
521
 
                return None
522
 
        return self.tree_file_id(trans_id)
523
 
 
524
 
    def inactive_file_id(self, trans_id):
525
 
        """Return the inactive file_id associated with a transaction id.
526
 
        That is, the one in the tree or in non_present_ids.
527
 
        The file_id may actually be active, too.
528
 
        """
529
 
        file_id = self.tree_file_id(trans_id)
530
 
        if file_id is not None:
531
 
            return file_id
532
 
        for key, value in viewitems(self._non_present_ids):
533
 
            if value == trans_id:
534
 
                return key
 
363
    def final_is_versioned(self, trans_id):
 
364
        raise NotImplementedError(self.final_is_versioned)
535
365
 
536
366
    def final_parent(self, trans_id):
537
367
        """Determine the parent file_id, after any changes are applied.
553
383
            except KeyError:
554
384
                raise NoFinalPath(trans_id, self)
555
385
 
556
 
    def by_parent(self):
557
 
        """Return a map of parent: children for known parents.
558
 
 
559
 
        Only new paths and parents of tree files with assigned ids are used.
560
 
        """
561
 
        by_parent = {}
562
 
        items = list(viewitems(self._new_parent))
563
 
        items.extend((t, self.final_parent(t))
564
 
                     for t in list(self._tree_id_paths))
565
 
        for trans_id, parent_id in items:
566
 
            if parent_id not in by_parent:
567
 
                by_parent[parent_id] = set()
568
 
            by_parent[parent_id].add(trans_id)
569
 
        return by_parent
570
 
 
571
386
    def path_changed(self, trans_id):
572
387
        """Return True if a trans_id's path has changed."""
573
388
        return (trans_id in self._new_name) or (trans_id in self._new_parent)
575
390
    def new_contents(self, trans_id):
576
391
        return (trans_id in self._new_contents)
577
392
 
578
 
    def find_conflicts(self):
 
393
    def find_raw_conflicts(self):
579
394
        """Find any violations of inventory or filesystem invariants"""
580
 
        if self._done is True:
581
 
            raise ReusingTransform()
582
 
        conflicts = []
583
 
        # ensure all children of all existent parents are known
584
 
        # all children of non-existent parents are known, by definition.
585
 
        self._add_tree_children()
586
 
        by_parent = self.by_parent()
587
 
        conflicts.extend(self._unversioned_parents(by_parent))
588
 
        conflicts.extend(self._parent_loops())
589
 
        conflicts.extend(self._duplicate_entries(by_parent))
590
 
        conflicts.extend(self._duplicate_ids())
591
 
        conflicts.extend(self._parent_type_conflicts(by_parent))
592
 
        conflicts.extend(self._improper_versioning())
593
 
        conflicts.extend(self._executability_conflicts())
594
 
        conflicts.extend(self._overwrite_conflicts())
595
 
        return conflicts
596
 
 
597
 
    def _check_malformed(self):
598
 
        conflicts = self.find_conflicts()
599
 
        if len(conflicts) != 0:
600
 
            raise MalformedTransform(conflicts=conflicts)
601
 
 
602
 
    def _add_tree_children(self):
603
 
        """Add all the children of all active parents to the known paths.
604
 
 
605
 
        Active parents are those which gain children, and those which are
606
 
        removed.  This is a necessary first step in detecting conflicts.
607
 
        """
608
 
        parents = list(self.by_parent())
609
 
        parents.extend([t for t in self._removed_contents if
610
 
                        self.tree_kind(t) == 'directory'])
611
 
        for trans_id in self._removed_id:
612
 
            path = self.tree_path(trans_id)
613
 
            if path is not None:
614
 
                if self._tree.stored_kind(path) == 'directory':
615
 
                    parents.append(trans_id)
616
 
            elif self.tree_kind(trans_id) == 'directory':
617
 
                parents.append(trans_id)
618
 
 
619
 
        for parent_id in parents:
620
 
            # ensure that all children are registered with the transaction
621
 
            list(self.iter_tree_children(parent_id))
622
 
 
623
 
    def _has_named_child(self, name, parent_id, known_children):
624
 
        """Does a parent already have a name child.
625
 
 
626
 
        :param name: The searched for name.
627
 
 
628
 
        :param parent_id: The parent for which the check is made.
629
 
 
630
 
        :param known_children: The already known children. This should have
631
 
            been recently obtained from `self.by_parent.get(parent_id)`
632
 
            (or will be if None is passed).
633
 
        """
634
 
        if known_children is None:
635
 
            known_children = self.by_parent().get(parent_id, [])
636
 
        for child in known_children:
637
 
            if self.final_name(child) == name:
638
 
                return True
639
 
        parent_path = self._tree_id_paths.get(parent_id, None)
640
 
        if parent_path is None:
641
 
            # No parent... no children
642
 
            return False
643
 
        child_path = joinpath(parent_path, name)
644
 
        child_id = self._tree_path_ids.get(child_path, None)
645
 
        if child_id is None:
646
 
            # Not known by the tree transform yet, check the filesystem
647
 
            return osutils.lexists(self._tree.abspath(child_path))
648
 
        else:
649
 
            raise AssertionError('child_id is missing: %s, %s, %s'
650
 
                                 % (name, parent_id, child_id))
651
 
 
652
 
    def _available_backup_name(self, name, target_id):
653
 
        """Find an available backup name.
654
 
 
655
 
        :param name: The basename of the file.
656
 
 
657
 
        :param target_id: The directory trans_id where the backup should
658
 
            be placed.
659
 
        """
660
 
        known_children = self.by_parent().get(target_id, [])
661
 
        return osutils.available_backup_name(
662
 
            name,
663
 
            lambda base: self._has_named_child(
664
 
                base, target_id, known_children))
665
 
 
666
 
    def _parent_loops(self):
667
 
        """No entry should be its own ancestor"""
668
 
        conflicts = []
669
 
        for trans_id in self._new_parent:
670
 
            seen = set()
671
 
            parent_id = trans_id
672
 
            while parent_id != ROOT_PARENT:
673
 
                seen.add(parent_id)
674
 
                try:
675
 
                    parent_id = self.final_parent(parent_id)
676
 
                except KeyError:
677
 
                    break
678
 
                if parent_id == trans_id:
679
 
                    conflicts.append(('parent loop', trans_id))
680
 
                if parent_id in seen:
681
 
                    break
682
 
        return conflicts
683
 
 
684
 
    def _unversioned_parents(self, by_parent):
685
 
        """If parent directories are versioned, children must be versioned."""
686
 
        conflicts = []
687
 
        for parent_id, children in viewitems(by_parent):
688
 
            if parent_id == ROOT_PARENT:
689
 
                continue
690
 
            if self.final_file_id(parent_id) is not None:
691
 
                continue
692
 
            for child_id in children:
693
 
                if self.final_file_id(child_id) is not None:
694
 
                    conflicts.append(('unversioned parent', parent_id))
695
 
                    break
696
 
        return conflicts
697
 
 
698
 
    def _improper_versioning(self):
699
 
        """Cannot version a file with no contents, or a bad type.
700
 
 
701
 
        However, existing entries with no contents are okay.
702
 
        """
703
 
        conflicts = []
704
 
        for trans_id in self._new_id:
705
 
            kind = self.final_kind(trans_id)
706
 
            if kind == 'symlink' and not self._tree.supports_symlinks():
707
 
                # Ignore symlinks as they are not supported on this platform
708
 
                continue
709
 
            if kind is None:
710
 
                conflicts.append(('versioning no contents', trans_id))
711
 
                continue
712
 
            if not self._tree.versionable_kind(kind):
713
 
                conflicts.append(('versioning bad kind', trans_id, kind))
714
 
        return conflicts
715
 
 
716
 
    def _executability_conflicts(self):
717
 
        """Check for bad executability changes.
718
 
 
719
 
        Only versioned files may have their executability set, because
720
 
        1. only versioned entries can have executability under windows
721
 
        2. only files can be executable.  (The execute bit on a directory
722
 
           does not indicate searchability)
723
 
        """
724
 
        conflicts = []
725
 
        for trans_id in self._new_executability:
726
 
            if self.final_file_id(trans_id) is None:
727
 
                conflicts.append(('unversioned executability', trans_id))
728
 
            else:
729
 
                if self.final_kind(trans_id) != "file":
730
 
                    conflicts.append(('non-file executability', trans_id))
731
 
        return conflicts
732
 
 
733
 
    def _overwrite_conflicts(self):
734
 
        """Check for overwrites (not permitted on Win32)"""
735
 
        conflicts = []
736
 
        for trans_id in self._new_contents:
737
 
            if self.tree_kind(trans_id) is None:
738
 
                continue
739
 
            if trans_id not in self._removed_contents:
740
 
                conflicts.append(('overwrite', trans_id,
741
 
                                  self.final_name(trans_id)))
742
 
        return conflicts
743
 
 
744
 
    def _duplicate_entries(self, by_parent):
745
 
        """No directory may have two entries with the same name."""
746
 
        conflicts = []
747
 
        if (self._new_name, self._new_parent) == ({}, {}):
748
 
            return conflicts
749
 
        for children in viewvalues(by_parent):
750
 
            name_ids = []
751
 
            for child_tid in children:
752
 
                name = self.final_name(child_tid)
753
 
                if name is not None:
754
 
                    # Keep children only if they still exist in the end
755
 
                    if not self._case_sensitive_target:
756
 
                        name = name.lower()
757
 
                    name_ids.append((name, child_tid))
758
 
            name_ids.sort()
759
 
            last_name = None
760
 
            last_trans_id = None
761
 
            for name, trans_id in name_ids:
762
 
                kind = self.final_kind(trans_id)
763
 
                file_id = self.final_file_id(trans_id)
764
 
                if kind is None and file_id is None:
765
 
                    continue
766
 
                if name == last_name:
767
 
                    conflicts.append(('duplicate', last_trans_id, trans_id,
768
 
                                      name))
769
 
                last_name = name
770
 
                last_trans_id = trans_id
771
 
        return conflicts
772
 
 
773
 
    def _duplicate_ids(self):
774
 
        """Each inventory id may only be used once"""
775
 
        conflicts = []
776
 
        try:
777
 
            all_ids = self._tree.all_file_ids()
778
 
        except errors.UnsupportedOperation:
779
 
            # it's okay for non-file-id trees to raise UnsupportedOperation.
780
 
            return []
781
 
        removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in
782
 
                                self._removed_id))
783
 
        active_tree_ids = all_ids.difference(removed_tree_ids)
784
 
        for trans_id, file_id in viewitems(self._new_id):
785
 
            if file_id in active_tree_ids:
786
 
                path = self._tree.id2path(file_id)
787
 
                old_trans_id = self.trans_id_tree_path(path)
788
 
                conflicts.append(('duplicate id', old_trans_id, trans_id))
789
 
        return conflicts
790
 
 
791
 
    def _parent_type_conflicts(self, by_parent):
792
 
        """Children must have a directory parent"""
793
 
        conflicts = []
794
 
        for parent_id, children in viewitems(by_parent):
795
 
            if parent_id == ROOT_PARENT:
796
 
                continue
797
 
            no_children = True
798
 
            for child_id in children:
799
 
                if self.final_kind(child_id) is not None:
800
 
                    no_children = False
801
 
                    break
802
 
            if no_children:
803
 
                continue
804
 
            # There is at least a child, so we need an existing directory to
805
 
            # contain it.
806
 
            kind = self.final_kind(parent_id)
807
 
            if kind is None:
808
 
                # The directory will be deleted
809
 
                conflicts.append(('missing parent', parent_id))
810
 
            elif kind != "directory":
811
 
                # Meh, we need a *directory* to put something in it
812
 
                conflicts.append(('non-directory parent', parent_id))
813
 
        return conflicts
814
 
 
815
 
    def _set_executability(self, path, trans_id):
816
 
        """Set the executability of versioned files """
817
 
        if self._tree._supports_executable():
818
 
            new_executability = self._new_executability[trans_id]
819
 
            abspath = self._tree.abspath(path)
820
 
            current_mode = os.stat(abspath).st_mode
821
 
            if new_executability:
822
 
                umask = os.umask(0)
823
 
                os.umask(umask)
824
 
                to_mode = current_mode | (0o100 & ~umask)
825
 
                # Enable x-bit for others only if they can read it.
826
 
                if current_mode & 0o004:
827
 
                    to_mode |= 0o001 & ~umask
828
 
                if current_mode & 0o040:
829
 
                    to_mode |= 0o010 & ~umask
830
 
            else:
831
 
                to_mode = current_mode & ~0o111
832
 
            osutils.chmod_if_possible(abspath, to_mode)
833
 
 
834
 
    def _new_entry(self, name, parent_id, file_id):
835
 
        """Helper function to create a new filesystem entry."""
836
 
        trans_id = self.create_path(name, parent_id)
837
 
        if file_id is not None:
838
 
            self.version_file(trans_id, file_id=file_id)
839
 
        return trans_id
 
395
        raise NotImplementedError(self.find_raw_conflicts)
840
396
 
841
397
    def new_file(self, name, parent_id, contents, file_id=None,
842
398
                 executable=None, sha1=None):
849
405
        :param file_id: The inventory ID of the file, if it is to be versioned.
850
406
        :param executable: Only valid when a file_id has been supplied.
851
407
        """
852
 
        trans_id = self._new_entry(name, parent_id, file_id)
853
 
        # TODO: rather than scheduling a set_executable call,
854
 
        # have create_file create the file with the right mode.
855
 
        self.create_file(contents, trans_id, sha1=sha1)
856
 
        if executable is not None:
857
 
            self.set_executability(executable, trans_id)
858
 
        return trans_id
 
408
        raise NotImplementedError(self.new_file)
859
409
 
860
410
    def new_directory(self, name, parent_id, file_id=None):
861
411
        """Convenience method to create directories.
865
415
        directory.
866
416
        file_id is the inventory ID of the directory, if it is to be versioned.
867
417
        """
868
 
        trans_id = self._new_entry(name, parent_id, file_id)
869
 
        self.create_directory(trans_id)
870
 
        return trans_id
 
418
        raise NotImplementedError(self.new_directory)
871
419
 
872
420
    def new_symlink(self, name, parent_id, target, file_id=None):
873
421
        """Convenience method to create symbolic link.
877
425
        target is a bytestring of the target of the symlink.
878
426
        file_id is the inventory ID of the file, if it is to be versioned.
879
427
        """
880
 
        trans_id = self._new_entry(name, parent_id, file_id)
881
 
        self.create_symlink(target, trans_id)
882
 
        return trans_id
 
428
        raise NotImplementedError(self.new_symlink)
883
429
 
884
430
    def new_orphan(self, trans_id, parent_id):
885
431
        """Schedule an item to be orphaned.
892
438
        """
893
439
        raise NotImplementedError(self.new_orphan)
894
440
 
895
 
    def _get_potential_orphans(self, dir_id):
896
 
        """Find the potential orphans in a directory.
897
 
 
898
 
        A directory can't be safely deleted if there are versioned files in it.
899
 
        If all the contained files are unversioned then they can be orphaned.
900
 
 
901
 
        The 'None' return value means that the directory contains at least one
902
 
        versioned file and should not be deleted.
903
 
 
904
 
        :param dir_id: The directory trans id.
905
 
 
906
 
        :return: A list of the orphan trans ids or None if at least one
907
 
             versioned file is present.
908
 
        """
909
 
        orphans = []
910
 
        # Find the potential orphans, stop if one item should be kept
911
 
        for child_tid in self.by_parent()[dir_id]:
912
 
            if child_tid in self._removed_contents:
913
 
                # The child is removed as part of the transform. Since it was
914
 
                # versioned before, it's not an orphan
915
 
                continue
916
 
            if self.final_file_id(child_tid) is None:
917
 
                # The child is not versioned
918
 
                orphans.append(child_tid)
919
 
            else:
920
 
                # We have a versioned file here, searching for orphans is
921
 
                # meaningless.
922
 
                orphans = None
923
 
                break
924
 
        return orphans
925
 
 
926
 
    def _affected_ids(self):
927
 
        """Return the set of transform ids affected by the transform"""
928
 
        trans_ids = set(self._removed_id)
929
 
        trans_ids.update(self._new_id)
930
 
        trans_ids.update(self._removed_contents)
931
 
        trans_ids.update(self._new_contents)
932
 
        trans_ids.update(self._new_executability)
933
 
        trans_ids.update(self._new_name)
934
 
        trans_ids.update(self._new_parent)
935
 
        return trans_ids
936
 
 
937
 
    def _get_file_id_maps(self):
938
 
        """Return mapping of file_ids to trans_ids in the to and from states"""
939
 
        trans_ids = self._affected_ids()
940
 
        from_trans_ids = {}
941
 
        to_trans_ids = {}
942
 
        # Build up two dicts: trans_ids associated with file ids in the
943
 
        # FROM state, vs the TO state.
944
 
        for trans_id in trans_ids:
945
 
            from_file_id = self.tree_file_id(trans_id)
946
 
            if from_file_id is not None:
947
 
                from_trans_ids[from_file_id] = trans_id
948
 
            to_file_id = self.final_file_id(trans_id)
949
 
            if to_file_id is not None:
950
 
                to_trans_ids[to_file_id] = trans_id
951
 
        return from_trans_ids, to_trans_ids
952
 
 
953
 
    def _from_file_data(self, from_trans_id, from_versioned, from_path):
954
 
        """Get data about a file in the from (tree) state
955
 
 
956
 
        Return a (name, parent, kind, executable) tuple
957
 
        """
958
 
        from_path = self._tree_id_paths.get(from_trans_id)
959
 
        if from_versioned:
960
 
            # get data from working tree if versioned
961
 
            from_entry = next(self._tree.iter_entries_by_dir(
962
 
                specific_files=[from_path]))[1]
963
 
            from_name = from_entry.name
964
 
            from_parent = from_entry.parent_id
965
 
        else:
966
 
            from_entry = None
967
 
            if from_path is None:
968
 
                # File does not exist in FROM state
969
 
                from_name = None
970
 
                from_parent = None
971
 
            else:
972
 
                # File exists, but is not versioned.  Have to use path-
973
 
                # splitting stuff
974
 
                from_name = os.path.basename(from_path)
975
 
                tree_parent = self.get_tree_parent(from_trans_id)
976
 
                from_parent = self.tree_file_id(tree_parent)
977
 
        if from_path is not None:
978
 
            from_kind, from_executable, from_stats = \
979
 
                self._tree._comparison_data(from_entry, from_path)
980
 
        else:
981
 
            from_kind = None
982
 
            from_executable = False
983
 
        return from_name, from_parent, from_kind, from_executable
984
 
 
985
 
    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
986
 
        """Get data about a file in the to (target) state
987
 
 
988
 
        Return a (name, parent, kind, executable) tuple
989
 
        """
990
 
        to_name = self.final_name(to_trans_id)
991
 
        to_kind = self.final_kind(to_trans_id)
992
 
        to_parent = self.final_file_id(self.final_parent(to_trans_id))
993
 
        if to_trans_id in self._new_executability:
994
 
            to_executable = self._new_executability[to_trans_id]
995
 
        elif to_trans_id == from_trans_id:
996
 
            to_executable = from_executable
997
 
        else:
998
 
            to_executable = False
999
 
        return to_name, to_parent, to_kind, to_executable
1000
 
 
1001
441
    def iter_changes(self):
1002
442
        """Produce output in the same format as Tree.iter_changes.
1003
443
 
1004
444
        Will produce nonsensical results if invoked while inventory/filesystem
1005
 
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
 
445
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
1006
446
 
1007
447
        This reads the Transform, but only reproduces changes involving a
1008
448
        file_id.  Files that are not versioned in either of the FROM or TO
1009
449
        states are not reflected.
1010
450
        """
1011
 
        final_paths = FinalPaths(self)
1012
 
        from_trans_ids, to_trans_ids = self._get_file_id_maps()
1013
 
        results = []
1014
 
        # Now iterate through all active file_ids
1015
 
        for file_id in set(from_trans_ids).union(to_trans_ids):
1016
 
            modified = False
1017
 
            from_trans_id = from_trans_ids.get(file_id)
1018
 
            # find file ids, and determine versioning state
1019
 
            if from_trans_id is None:
1020
 
                from_versioned = False
1021
 
                from_trans_id = to_trans_ids[file_id]
1022
 
            else:
1023
 
                from_versioned = True
1024
 
            to_trans_id = to_trans_ids.get(file_id)
1025
 
            if to_trans_id is None:
1026
 
                to_versioned = False
1027
 
                to_trans_id = from_trans_id
1028
 
            else:
1029
 
                to_versioned = True
1030
 
 
1031
 
            if not from_versioned:
1032
 
                from_path = None
1033
 
            else:
1034
 
                from_path = self._tree_id_paths.get(from_trans_id)
1035
 
            if not to_versioned:
1036
 
                to_path = None
1037
 
            else:
1038
 
                to_path = final_paths.get_path(to_trans_id)
1039
 
 
1040
 
            from_name, from_parent, from_kind, from_executable = \
1041
 
                self._from_file_data(from_trans_id, from_versioned, from_path)
1042
 
 
1043
 
            to_name, to_parent, to_kind, to_executable = \
1044
 
                self._to_file_data(to_trans_id, from_trans_id, from_executable)
1045
 
 
1046
 
            if from_kind != to_kind:
1047
 
                modified = True
1048
 
            elif to_kind in ('file', 'symlink') and (
1049
 
                    to_trans_id != from_trans_id
1050
 
                    or to_trans_id in self._new_contents):
1051
 
                modified = True
1052
 
            if (not modified and from_versioned == to_versioned
1053
 
                and from_parent == to_parent and from_name == to_name
1054
 
                    and from_executable == to_executable):
1055
 
                continue
1056
 
            results.append(
1057
 
                TreeChange(
1058
 
                    file_id, (from_path, to_path), modified,
1059
 
                    (from_versioned, to_versioned),
1060
 
                    (from_parent, to_parent),
1061
 
                    (from_name, to_name),
1062
 
                    (from_kind, to_kind),
1063
 
                    (from_executable, to_executable)))
1064
 
 
1065
 
        def path_key(c):
1066
 
            return (c.path[0] or '', c.path[1] or '')
1067
 
        return iter(sorted(results, key=path_key))
 
451
        raise NotImplementedError(self.iter_changes)
1068
452
 
1069
453
    def get_preview_tree(self):
1070
454
        """Return a tree representing the result of the transform.
1072
456
        The tree is a snapshot, and altering the TreeTransform will invalidate
1073
457
        it.
1074
458
        """
1075
 
        return _PreviewTree(self)
 
459
        raise NotImplementedError(self.get_preview_tree)
1076
460
 
1077
461
    def commit(self, branch, message, merge_parents=None, strict=False,
1078
462
               timestamp=None, timezone=None, committer=None, authors=None,
1097
481
            may reduce performance for some non-native formats.)
1098
482
        :return: The revision_id of the revision committed.
1099
483
        """
1100
 
        self._check_malformed()
1101
 
        if strict:
1102
 
            unversioned = set(self._new_contents).difference(set(self._new_id))
1103
 
            for trans_id in unversioned:
1104
 
                if self.final_file_id(trans_id) is None:
1105
 
                    raise errors.StrictCommitFailed()
1106
 
 
1107
 
        revno, last_rev_id = branch.last_revision_info()
1108
 
        if last_rev_id == _mod_revision.NULL_REVISION:
1109
 
            if merge_parents is not None:
1110
 
                raise ValueError('Cannot supply merge parents for first'
1111
 
                                 ' commit.')
1112
 
            parent_ids = []
1113
 
        else:
1114
 
            parent_ids = [last_rev_id]
1115
 
            if merge_parents is not None:
1116
 
                parent_ids.extend(merge_parents)
1117
 
        if self._tree.get_revision_id() != last_rev_id:
1118
 
            raise ValueError('TreeTransform not based on branch basis: %s' %
1119
 
                             self._tree.get_revision_id().decode('utf-8'))
1120
 
        revprops = commit.Commit.update_revprops(revprops, branch, authors)
1121
 
        builder = branch.get_commit_builder(parent_ids,
1122
 
                                            timestamp=timestamp,
1123
 
                                            timezone=timezone,
1124
 
                                            committer=committer,
1125
 
                                            revprops=revprops,
1126
 
                                            revision_id=revision_id)
1127
 
        preview = self.get_preview_tree()
1128
 
        list(builder.record_iter_changes(preview, last_rev_id,
1129
 
                                         self.iter_changes()))
1130
 
        builder.finish_inventory()
1131
 
        revision_id = builder.commit(message)
1132
 
        branch.set_last_revision_info(revno + 1, revision_id)
1133
 
        return revision_id
1134
 
 
1135
 
    def _text_parent(self, trans_id):
1136
 
        path = self.tree_path(trans_id)
1137
 
        try:
1138
 
            if path is None or self._tree.kind(path) != 'file':
1139
 
                return None
1140
 
        except errors.NoSuchFile:
1141
 
            return None
1142
 
        return path
1143
 
 
1144
 
    def _get_parents_texts(self, trans_id):
1145
 
        """Get texts for compression parents of this file."""
1146
 
        path = self._text_parent(trans_id)
1147
 
        if path is None:
1148
 
            return ()
1149
 
        return (self._tree.get_file_text(path),)
1150
 
 
1151
 
    def _get_parents_lines(self, trans_id):
1152
 
        """Get lines for compression parents of this file."""
1153
 
        path = self._text_parent(trans_id)
1154
 
        if path is None:
1155
 
            return ()
1156
 
        return (self._tree.get_file_lines(path),)
1157
 
 
1158
 
    def serialize(self, serializer):
1159
 
        """Serialize this TreeTransform.
1160
 
 
1161
 
        :param serializer: A Serialiser like pack.ContainerSerializer.
1162
 
        """
1163
 
        from . import bencode
1164
 
        new_name = {k.encode('utf-8'): v.encode('utf-8')
1165
 
                    for k, v in viewitems(self._new_name)}
1166
 
        new_parent = {k.encode('utf-8'): v.encode('utf-8')
1167
 
                      for k, v in viewitems(self._new_parent)}
1168
 
        new_id = {k.encode('utf-8'): v
1169
 
                  for k, v in viewitems(self._new_id)}
1170
 
        new_executability = {k.encode('utf-8'): int(v)
1171
 
                             for k, v in viewitems(self._new_executability)}
1172
 
        tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
1173
 
                         for k, v in viewitems(self._tree_path_ids)}
1174
 
        non_present_ids = {k: v.encode('utf-8')
1175
 
                           for k, v in viewitems(self._non_present_ids)}
1176
 
        removed_contents = [trans_id.encode('utf-8')
1177
 
                            for trans_id in self._removed_contents]
1178
 
        removed_id = [trans_id.encode('utf-8')
1179
 
                      for trans_id in self._removed_id]
1180
 
        attribs = {
1181
 
            b'_id_number': self._id_number,
1182
 
            b'_new_name': new_name,
1183
 
            b'_new_parent': new_parent,
1184
 
            b'_new_executability': new_executability,
1185
 
            b'_new_id': new_id,
1186
 
            b'_tree_path_ids': tree_path_ids,
1187
 
            b'_removed_id': removed_id,
1188
 
            b'_removed_contents': removed_contents,
1189
 
            b'_non_present_ids': non_present_ids,
1190
 
            }
1191
 
        yield serializer.bytes_record(bencode.bencode(attribs),
1192
 
                                      ((b'attribs',),))
1193
 
        for trans_id, kind in sorted(viewitems(self._new_contents)):
1194
 
            if kind == 'file':
1195
 
                with open(self._limbo_name(trans_id), 'rb') as cur_file:
1196
 
                    lines = cur_file.readlines()
1197
 
                parents = self._get_parents_lines(trans_id)
1198
 
                mpdiff = multiparent.MultiParent.from_lines(lines, parents)
1199
 
                content = b''.join(mpdiff.to_patch())
1200
 
            if kind == 'directory':
1201
 
                content = b''
1202
 
            if kind == 'symlink':
1203
 
                content = self._read_symlink_target(trans_id)
1204
 
                if not isinstance(content, bytes):
1205
 
                    content = content.encode('utf-8')
1206
 
            yield serializer.bytes_record(
1207
 
                content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
1208
 
 
1209
 
    def deserialize(self, records):
1210
 
        """Deserialize a stored TreeTransform.
1211
 
 
1212
 
        :param records: An iterable of (names, content) tuples, as per
1213
 
            pack.ContainerPushParser.
1214
 
        """
1215
 
        from . import bencode
1216
 
        names, content = next(records)
1217
 
        attribs = bencode.bdecode(content)
1218
 
        self._id_number = attribs[b'_id_number']
1219
 
        self._new_name = {k.decode('utf-8'): v.decode('utf-8')
1220
 
                          for k, v in viewitems(attribs[b'_new_name'])}
1221
 
        self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
1222
 
                            for k, v in viewitems(attribs[b'_new_parent'])}
1223
 
        self._new_executability = {
1224
 
            k.decode('utf-8'): bool(v)
1225
 
            for k, v in viewitems(attribs[b'_new_executability'])}
1226
 
        self._new_id = {k.decode('utf-8'): v
1227
 
                        for k, v in viewitems(attribs[b'_new_id'])}
1228
 
        self._r_new_id = {v: k for k, v in viewitems(self._new_id)}
1229
 
        self._tree_path_ids = {}
1230
 
        self._tree_id_paths = {}
1231
 
        for bytepath, trans_id in viewitems(attribs[b'_tree_path_ids']):
1232
 
            path = bytepath.decode('utf-8')
1233
 
            trans_id = trans_id.decode('utf-8')
1234
 
            self._tree_path_ids[path] = trans_id
1235
 
            self._tree_id_paths[trans_id] = path
1236
 
        self._removed_id = {trans_id.decode('utf-8')
1237
 
                            for trans_id in attribs[b'_removed_id']}
1238
 
        self._removed_contents = set(
1239
 
            trans_id.decode('utf-8')
1240
 
            for trans_id in attribs[b'_removed_contents'])
1241
 
        self._non_present_ids = {
1242
 
            k: v.decode('utf-8')
1243
 
            for k, v in viewitems(attribs[b'_non_present_ids'])}
1244
 
        for ((trans_id, kind),), content in records:
1245
 
            trans_id = trans_id.decode('utf-8')
1246
 
            kind = kind.decode('ascii')
1247
 
            if kind == 'file':
1248
 
                mpdiff = multiparent.MultiParent.from_patch(content)
1249
 
                lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
1250
 
                self.create_file(lines, trans_id)
1251
 
            if kind == 'directory':
1252
 
                self.create_directory(trans_id)
1253
 
            if kind == 'symlink':
1254
 
                self.create_symlink(content.decode('utf-8'), trans_id)
 
484
        raise NotImplementedError(self.commit)
1255
485
 
1256
486
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1257
487
        """Schedule creation of a new file.
1292
522
        """Cancel the creation of new file contents."""
1293
523
        raise NotImplementedError(self.cancel_creation)
1294
524
 
1295
 
 
1296
 
class DiskTreeTransform(TreeTransformBase):
1297
 
    """Tree transform storing its contents on disk."""
1298
 
 
1299
 
    def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1300
 
        """Constructor.
1301
 
        :param tree: The tree that will be transformed, but not necessarily
1302
 
            the output tree.
1303
 
        :param limbodir: A directory where new files can be stored until
1304
 
            they are installed in their proper places
1305
 
        :param pb: ignored
1306
 
        :param case_sensitive: If True, the target of the transform is
1307
 
            case sensitive, not just case preserving.
1308
 
        """
1309
 
        TreeTransformBase.__init__(self, tree, pb, case_sensitive)
1310
 
        self._limbodir = limbodir
1311
 
        self._deletiondir = None
1312
 
        # A mapping of transform ids to their limbo filename
1313
 
        self._limbo_files = {}
1314
 
        self._possibly_stale_limbo_files = set()
1315
 
        # A mapping of transform ids to a set of the transform ids of children
1316
 
        # that their limbo directory has
1317
 
        self._limbo_children = {}
1318
 
        # Map transform ids to maps of child filename to child transform id
1319
 
        self._limbo_children_names = {}
1320
 
        # List of transform ids that need to be renamed from limbo into place
1321
 
        self._needs_rename = set()
1322
 
        self._creation_mtime = None
1323
 
        self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1324
 
 
1325
 
    def finalize(self):
1326
 
        """Release the working tree lock, if held, clean up limbo dir.
1327
 
 
1328
 
        This is required if apply has not been invoked, but can be invoked
1329
 
        even after apply.
1330
 
        """
1331
 
        if self._tree is None:
1332
 
            return
1333
 
        try:
1334
 
            limbo_paths = list(viewvalues(self._limbo_files))
1335
 
            limbo_paths.extend(self._possibly_stale_limbo_files)
1336
 
            limbo_paths.sort(reverse=True)
1337
 
            for path in limbo_paths:
1338
 
                try:
1339
 
                    delete_any(path)
1340
 
                except OSError as e:
1341
 
                    if e.errno != errno.ENOENT:
1342
 
                        raise
1343
 
                    # XXX: warn? perhaps we just got interrupted at an
1344
 
                    # inconvenient moment, but perhaps files are disappearing
1345
 
                    # from under us?
1346
 
            try:
1347
 
                delete_any(self._limbodir)
1348
 
            except OSError:
1349
 
                # We don't especially care *why* the dir is immortal.
1350
 
                raise ImmortalLimbo(self._limbodir)
1351
 
            try:
1352
 
                if self._deletiondir is not None:
1353
 
                    delete_any(self._deletiondir)
1354
 
            except OSError:
1355
 
                raise errors.ImmortalPendingDeletion(self._deletiondir)
1356
 
        finally:
1357
 
            TreeTransformBase.finalize(self)
1358
 
 
1359
 
    def _limbo_supports_executable(self):
1360
 
        """Check if the limbo path supports the executable bit."""
1361
 
        return osutils.supports_executable(self._limbodir)
1362
 
 
1363
 
    def _limbo_name(self, trans_id):
1364
 
        """Generate the limbo name of a file"""
1365
 
        limbo_name = self._limbo_files.get(trans_id)
1366
 
        if limbo_name is None:
1367
 
            limbo_name = self._generate_limbo_path(trans_id)
1368
 
            self._limbo_files[trans_id] = limbo_name
1369
 
        return limbo_name
1370
 
 
1371
 
    def _generate_limbo_path(self, trans_id):
1372
 
        """Generate a limbo path using the trans_id as the relative path.
1373
 
 
1374
 
        This is suitable as a fallback, and when the transform should not be
1375
 
        sensitive to the path encoding of the limbo directory.
1376
 
        """
1377
 
        self._needs_rename.add(trans_id)
1378
 
        return pathjoin(self._limbodir, trans_id)
1379
 
 
1380
 
    def adjust_path(self, name, parent, trans_id):
1381
 
        previous_parent = self._new_parent.get(trans_id)
1382
 
        previous_name = self._new_name.get(trans_id)
1383
 
        TreeTransformBase.adjust_path(self, name, parent, trans_id)
1384
 
        if (trans_id in self._limbo_files
1385
 
                and trans_id not in self._needs_rename):
1386
 
            self._rename_in_limbo([trans_id])
1387
 
            if previous_parent != parent:
1388
 
                self._limbo_children[previous_parent].remove(trans_id)
1389
 
            if previous_parent != parent or previous_name != name:
1390
 
                del self._limbo_children_names[previous_parent][previous_name]
1391
 
 
1392
 
    def _rename_in_limbo(self, trans_ids):
1393
 
        """Fix limbo names so that the right final path is produced.
1394
 
 
1395
 
        This means we outsmarted ourselves-- we tried to avoid renaming
1396
 
        these files later by creating them with their final names in their
1397
 
        final parents.  But now the previous name or parent is no longer
1398
 
        suitable, so we have to rename them.
1399
 
 
1400
 
        Even for trans_ids that have no new contents, we must remove their
1401
 
        entries from _limbo_files, because they are now stale.
1402
 
        """
1403
 
        for trans_id in trans_ids:
1404
 
            old_path = self._limbo_files[trans_id]
1405
 
            self._possibly_stale_limbo_files.add(old_path)
1406
 
            del self._limbo_files[trans_id]
1407
 
            if trans_id not in self._new_contents:
1408
 
                continue
1409
 
            new_path = self._limbo_name(trans_id)
1410
 
            os.rename(old_path, new_path)
1411
 
            self._possibly_stale_limbo_files.remove(old_path)
1412
 
            for descendant in self._limbo_descendants(trans_id):
1413
 
                desc_path = self._limbo_files[descendant]
1414
 
                desc_path = new_path + desc_path[len(old_path):]
1415
 
                self._limbo_files[descendant] = desc_path
1416
 
 
1417
 
    def _limbo_descendants(self, trans_id):
1418
 
        """Return the set of trans_ids whose limbo paths descend from this."""
1419
 
        descendants = set(self._limbo_children.get(trans_id, []))
1420
 
        for descendant in list(descendants):
1421
 
            descendants.update(self._limbo_descendants(descendant))
1422
 
        return descendants
1423
 
 
1424
 
    def _set_mode(self, trans_id, mode_id, typefunc):
1425
 
        raise NotImplementedError(self._set_mode)
1426
 
 
1427
 
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
1428
 
        """Schedule creation of a new file.
1429
 
 
1430
 
        :seealso: new_file.
1431
 
 
1432
 
        :param contents: an iterator of strings, all of which will be written
1433
 
            to the target destination.
1434
 
        :param trans_id: TreeTransform handle
1435
 
        :param mode_id: If not None, force the mode of the target file to match
1436
 
            the mode of the object referenced by mode_id.
1437
 
            Otherwise, we will try to preserve mode bits of an existing file.
1438
 
        :param sha1: If the sha1 of this content is already known, pass it in.
1439
 
            We can use it to prevent future sha1 computations.
1440
 
        """
1441
 
        name = self._limbo_name(trans_id)
1442
 
        with open(name, 'wb') as f:
1443
 
            unique_add(self._new_contents, trans_id, 'file')
1444
 
            f.writelines(contents)
1445
 
        self._set_mtime(name)
1446
 
        self._set_mode(trans_id, mode_id, S_ISREG)
1447
 
        # It is unfortunate we have to use lstat instead of fstat, but we just
1448
 
        # used utime and chmod on the file, so we need the accurate final
1449
 
        # details.
1450
 
        if sha1 is not None:
1451
 
            self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
1452
 
 
1453
 
    def _read_symlink_target(self, trans_id):
1454
 
        return os.readlink(self._limbo_name(trans_id))
1455
 
 
1456
 
    def _set_mtime(self, path):
1457
 
        """All files that are created get the same mtime.
1458
 
 
1459
 
        This time is set by the first object to be created.
1460
 
        """
1461
 
        if self._creation_mtime is None:
1462
 
            self._creation_mtime = time.time()
1463
 
        os.utime(path, (self._creation_mtime, self._creation_mtime))
1464
 
 
1465
 
    def create_hardlink(self, path, trans_id):
1466
 
        """Schedule creation of a hard link"""
1467
 
        name = self._limbo_name(trans_id)
1468
 
        try:
1469
 
            os.link(path, name)
1470
 
        except OSError as e:
1471
 
            if e.errno != errno.EPERM:
1472
 
                raise
1473
 
            raise errors.HardLinkNotSupported(path)
1474
 
        try:
1475
 
            unique_add(self._new_contents, trans_id, 'file')
1476
 
        except BaseException:
1477
 
            # Clean up the file, it never got registered so
1478
 
            # TreeTransform.finalize() won't clean it up.
1479
 
            os.unlink(name)
1480
 
            raise
1481
 
 
1482
 
    def create_directory(self, trans_id):
1483
 
        """Schedule creation of a new directory.
1484
 
 
1485
 
        See also new_directory.
1486
 
        """
1487
 
        os.mkdir(self._limbo_name(trans_id))
1488
 
        unique_add(self._new_contents, trans_id, 'directory')
1489
 
 
1490
 
    def create_symlink(self, target, trans_id):
1491
 
        """Schedule creation of a new symbolic link.
1492
 
 
1493
 
        target is a bytestring.
1494
 
        See also new_symlink.
1495
 
        """
1496
 
        if self._create_symlinks:
1497
 
            os.symlink(target, self._limbo_name(trans_id))
1498
 
        else:
1499
 
            try:
1500
 
                path = FinalPaths(self).get_path(trans_id)
1501
 
            except KeyError:
1502
 
                path = None
1503
 
            trace.warning(
1504
 
                'Unable to create symlink "%s" on this filesystem.' % (path,))
1505
 
        # We add symlink to _new_contents even if they are unsupported
1506
 
        # and not created. These entries are subsequently used to avoid
1507
 
        # conflicts on platforms that don't support symlink
1508
 
        unique_add(self._new_contents, trans_id, 'symlink')
1509
 
 
1510
 
    def cancel_creation(self, trans_id):
1511
 
        """Cancel the creation of new file contents."""
1512
 
        del self._new_contents[trans_id]
1513
 
        if trans_id in self._observed_sha1s:
1514
 
            del self._observed_sha1s[trans_id]
1515
 
        children = self._limbo_children.get(trans_id)
1516
 
        # if this is a limbo directory with children, move them before removing
1517
 
        # the directory
1518
 
        if children is not None:
1519
 
            self._rename_in_limbo(children)
1520
 
            del self._limbo_children[trans_id]
1521
 
            del self._limbo_children_names[trans_id]
1522
 
        delete_any(self._limbo_name(trans_id))
1523
 
 
1524
 
    def new_orphan(self, trans_id, parent_id):
1525
 
        conf = self._tree.get_config_stack()
1526
 
        handle_orphan = conf.get('transform.orphan_policy')
1527
 
        handle_orphan(self, trans_id, parent_id)
 
525
    def cook_conflicts(self, raw_conflicts):
 
526
        """Cook conflicts.
 
527
        """
 
528
        raise NotImplementedError(self.cook_conflicts)
1528
529
 
1529
530
 
1530
531
class OrphaningError(errors.BzrError):
1598
599
    invalid='warning')
1599
600
 
1600
601
 
1601
 
class TreeTransform(DiskTreeTransform):
1602
 
    """Represent a tree transformation.
1603
 
 
1604
 
    This object is designed to support incremental generation of the transform,
1605
 
    in any order.
1606
 
 
1607
 
    However, it gives optimum performance when parent directories are created
1608
 
    before their contents.  The transform is then able to put child files
1609
 
    directly in their parent directory, avoiding later renames.
1610
 
 
1611
 
    It is easy to produce malformed transforms, but they are generally
1612
 
    harmless.  Attempting to apply a malformed transform will cause an
1613
 
    exception to be raised before any modifications are made to the tree.
1614
 
 
1615
 
    Many kinds of malformed transforms can be corrected with the
1616
 
    resolve_conflicts function.  The remaining ones indicate programming error,
1617
 
    such as trying to create a file with no path.
1618
 
 
1619
 
    Two sets of file creation methods are supplied.  Convenience methods are:
1620
 
     * new_file
1621
 
     * new_directory
1622
 
     * new_symlink
1623
 
 
1624
 
    These are composed of the low-level methods:
1625
 
     * create_path
1626
 
     * create_file or create_directory or create_symlink
1627
 
     * version_file
1628
 
     * set_executability
1629
 
 
1630
 
    Transform/Transaction ids
1631
 
    -------------------------
1632
 
    trans_ids are temporary ids assigned to all files involved in a transform.
1633
 
    It's possible, even common, that not all files in the Tree have trans_ids.
1634
 
 
1635
 
    trans_ids are used because filenames and file_ids are not good enough
1636
 
    identifiers; filenames change, and not all files have file_ids.  File-ids
1637
 
    are also associated with trans-ids, so that moving a file moves its
1638
 
    file-id.
1639
 
 
1640
 
    trans_ids are only valid for the TreeTransform that generated them.
1641
 
 
1642
 
    Limbo
1643
 
    -----
1644
 
    Limbo is a temporary directory use to hold new versions of files.
1645
 
    Files are added to limbo by create_file, create_directory, create_symlink,
1646
 
    and their convenience variants (new_*).  Files may be removed from limbo
1647
 
    using cancel_creation.  Files are renamed from limbo into their final
1648
 
    location as part of TreeTransform.apply
1649
 
 
1650
 
    Limbo must be cleaned up, by either calling TreeTransform.apply or
1651
 
    calling TreeTransform.finalize.
1652
 
 
1653
 
    Files are placed into limbo inside their parent directories, where
1654
 
    possible.  This reduces subsequent renames, and makes operations involving
1655
 
    lots of files faster.  This optimization is only possible if the parent
1656
 
    directory is created *before* creating any of its children, so avoid
1657
 
    creating children before parents, where possible.
1658
 
 
1659
 
    Pending-deletion
1660
 
    ----------------
1661
 
    This temporary directory is used by _FileMover for storing files that are
1662
 
    about to be deleted.  In case of rollback, the files will be restored.
1663
 
    FileMover does not delete files until it is sure that a rollback will not
1664
 
    happen.
1665
 
    """
1666
 
 
1667
 
    def __init__(self, tree, pb=None):
1668
 
        """Note: a tree_write lock is taken on the tree.
1669
 
 
1670
 
        Use TreeTransform.finalize() to release the lock (can be omitted if
1671
 
        TreeTransform.apply() called).
1672
 
        """
1673
 
        tree.lock_tree_write()
1674
 
        try:
1675
 
            limbodir = urlutils.local_path_from_url(
1676
 
                tree._transport.abspath('limbo'))
1677
 
            osutils.ensure_empty_directory_exists(
1678
 
                limbodir,
1679
 
                errors.ExistingLimbo)
1680
 
            deletiondir = urlutils.local_path_from_url(
1681
 
                tree._transport.abspath('pending-deletion'))
1682
 
            osutils.ensure_empty_directory_exists(
1683
 
                deletiondir,
1684
 
                errors.ExistingPendingDeletion)
1685
 
        except BaseException:
1686
 
            tree.unlock()
1687
 
            raise
1688
 
 
1689
 
        # Cache of realpath results, to speed up canonical_path
1690
 
        self._realpaths = {}
1691
 
        # Cache of relpath results, to speed up canonical_path
1692
 
        self._relpaths = {}
1693
 
        DiskTreeTransform.__init__(self, tree, limbodir, pb,
1694
 
                                   tree.case_sensitive)
1695
 
        self._deletiondir = deletiondir
1696
 
 
1697
 
    def canonical_path(self, path):
1698
 
        """Get the canonical tree-relative path"""
1699
 
        # don't follow final symlinks
1700
 
        abs = self._tree.abspath(path)
1701
 
        if abs in self._relpaths:
1702
 
            return self._relpaths[abs]
1703
 
        dirname, basename = os.path.split(abs)
1704
 
        if dirname not in self._realpaths:
1705
 
            self._realpaths[dirname] = os.path.realpath(dirname)
1706
 
        dirname = self._realpaths[dirname]
1707
 
        abs = pathjoin(dirname, basename)
1708
 
        if dirname in self._relpaths:
1709
 
            relpath = pathjoin(self._relpaths[dirname], basename)
1710
 
            relpath = relpath.rstrip('/\\')
1711
 
        else:
1712
 
            relpath = self._tree.relpath(abs)
1713
 
        self._relpaths[abs] = relpath
1714
 
        return relpath
1715
 
 
1716
 
    def tree_kind(self, trans_id):
1717
 
        """Determine the file kind in the working tree.
1718
 
 
1719
 
        :returns: The file kind or None if the file does not exist
1720
 
        """
1721
 
        path = self._tree_id_paths.get(trans_id)
1722
 
        if path is None:
1723
 
            return None
1724
 
        try:
1725
 
            return file_kind(self._tree.abspath(path))
1726
 
        except errors.NoSuchFile:
1727
 
            return None
1728
 
 
1729
 
    def _set_mode(self, trans_id, mode_id, typefunc):
1730
 
        """Set the mode of new file contents.
1731
 
        The mode_id is the existing file to get the mode from (often the same
1732
 
        as trans_id).  The operation is only performed if there's a mode match
1733
 
        according to typefunc.
1734
 
        """
1735
 
        if mode_id is None:
1736
 
            mode_id = trans_id
1737
 
        try:
1738
 
            old_path = self._tree_id_paths[mode_id]
1739
 
        except KeyError:
1740
 
            return
1741
 
        try:
1742
 
            mode = os.stat(self._tree.abspath(old_path)).st_mode
1743
 
        except OSError as e:
1744
 
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
1745
 
                # Either old_path doesn't exist, or the parent of the
1746
 
                # target is not a directory (but will be one eventually)
1747
 
                # Either way, we know it doesn't exist *right now*
1748
 
                # See also bug #248448
1749
 
                return
1750
 
            else:
1751
 
                raise
1752
 
        if typefunc(mode):
1753
 
            osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
1754
 
 
1755
 
    def iter_tree_children(self, parent_id):
1756
 
        """Iterate through the entry's tree children, if any"""
1757
 
        try:
1758
 
            path = self._tree_id_paths[parent_id]
1759
 
        except KeyError:
1760
 
            return
1761
 
        try:
1762
 
            children = os.listdir(self._tree.abspath(path))
1763
 
        except OSError as e:
1764
 
            if not (osutils._is_error_enotdir(e) or
1765
 
                    e.errno in (errno.ENOENT, errno.ESRCH)):
1766
 
                raise
1767
 
            return
1768
 
 
1769
 
        for child in children:
1770
 
            childpath = joinpath(path, child)
1771
 
            if self._tree.is_control_filename(childpath):
1772
 
                continue
1773
 
            yield self.trans_id_tree_path(childpath)
1774
 
 
1775
 
    def _generate_limbo_path(self, trans_id):
1776
 
        """Generate a limbo path using the final path if possible.
1777
 
 
1778
 
        This optimizes the performance of applying the tree transform by
1779
 
        avoiding renames.  These renames can be avoided only when the parent
1780
 
        directory is already scheduled for creation.
1781
 
 
1782
 
        If the final path cannot be used, falls back to using the trans_id as
1783
 
        the relpath.
1784
 
        """
1785
 
        parent = self._new_parent.get(trans_id)
1786
 
        # if the parent directory is already in limbo (e.g. when building a
1787
 
        # tree), choose a limbo name inside the parent, to reduce further
1788
 
        # renames.
1789
 
        use_direct_path = False
1790
 
        if self._new_contents.get(parent) == 'directory':
1791
 
            filename = self._new_name.get(trans_id)
1792
 
            if filename is not None:
1793
 
                if parent not in self._limbo_children:
1794
 
                    self._limbo_children[parent] = set()
1795
 
                    self._limbo_children_names[parent] = {}
1796
 
                    use_direct_path = True
1797
 
                # the direct path can only be used if no other file has
1798
 
                # already taken this pathname, i.e. if the name is unused, or
1799
 
                # if it is already associated with this trans_id.
1800
 
                elif self._case_sensitive_target:
1801
 
                    if (self._limbo_children_names[parent].get(filename)
1802
 
                            in (trans_id, None)):
1803
 
                        use_direct_path = True
1804
 
                else:
1805
 
                    for l_filename, l_trans_id in viewitems(
1806
 
                            self._limbo_children_names[parent]):
1807
 
                        if l_trans_id == trans_id:
1808
 
                            continue
1809
 
                        if l_filename.lower() == filename.lower():
1810
 
                            break
1811
 
                    else:
1812
 
                        use_direct_path = True
1813
 
 
1814
 
        if not use_direct_path:
1815
 
            return DiskTreeTransform._generate_limbo_path(self, trans_id)
1816
 
 
1817
 
        limbo_name = pathjoin(self._limbo_files[parent], filename)
1818
 
        self._limbo_children[parent].add(trans_id)
1819
 
        self._limbo_children_names[parent][filename] = trans_id
1820
 
        return limbo_name
1821
 
 
1822
 
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
1823
 
        """Apply all changes to the inventory and filesystem.
1824
 
 
1825
 
        If filesystem or inventory conflicts are present, MalformedTransform
1826
 
        will be thrown.
1827
 
 
1828
 
        If apply succeeds, finalize is not necessary.
1829
 
 
1830
 
        :param no_conflicts: if True, the caller guarantees there are no
1831
 
            conflicts, so no check is made.
1832
 
        :param precomputed_delta: An inventory delta to use instead of
1833
 
            calculating one.
1834
 
        :param _mover: Supply an alternate FileMover, for testing
1835
 
        """
1836
 
        for hook in MutableTree.hooks['pre_transform']:
1837
 
            hook(self._tree, self)
1838
 
        if not no_conflicts:
1839
 
            self._check_malformed()
1840
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1841
 
            if precomputed_delta is None:
1842
 
                child_pb.update(gettext('Apply phase'), 0, 2)
1843
 
                inventory_delta = self._generate_inventory_delta()
1844
 
                offset = 1
1845
 
            else:
1846
 
                inventory_delta = precomputed_delta
1847
 
                offset = 0
1848
 
            if _mover is None:
1849
 
                mover = _FileMover()
1850
 
            else:
1851
 
                mover = _mover
1852
 
            try:
1853
 
                child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
1854
 
                self._apply_removals(mover)
1855
 
                child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
1856
 
                modified_paths = self._apply_insertions(mover)
1857
 
            except BaseException:
1858
 
                mover.rollback()
1859
 
                raise
1860
 
            else:
1861
 
                mover.apply_deletions()
1862
 
        if self.final_file_id(self.root) is None:
1863
 
            inventory_delta = [e for e in inventory_delta if e[0] != '']
1864
 
        self._tree.apply_inventory_delta(inventory_delta)
1865
 
        self._apply_observed_sha1s()
1866
 
        self._done = True
1867
 
        self.finalize()
1868
 
        return _TransformResults(modified_paths, self.rename_count)
1869
 
 
1870
 
    def _generate_inventory_delta(self):
1871
 
        """Generate an inventory delta for the current transform."""
1872
 
        inventory_delta = []
1873
 
        new_paths = self._inventory_altered()
1874
 
        total_entries = len(new_paths) + len(self._removed_id)
1875
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1876
 
            for num, trans_id in enumerate(self._removed_id):
1877
 
                if (num % 10) == 0:
1878
 
                    child_pb.update(gettext('removing file'),
1879
 
                                    num, total_entries)
1880
 
                if trans_id == self._new_root:
1881
 
                    file_id = self._tree.path2id('')
1882
 
                else:
1883
 
                    file_id = self.tree_file_id(trans_id)
1884
 
                # File-id isn't really being deleted, just moved
1885
 
                if file_id in self._r_new_id:
1886
 
                    continue
1887
 
                path = self._tree_id_paths[trans_id]
1888
 
                inventory_delta.append((path, None, file_id, None))
1889
 
            new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1890
 
                                     new_paths)
1891
 
            for num, (path, trans_id) in enumerate(new_paths):
1892
 
                if (num % 10) == 0:
1893
 
                    child_pb.update(gettext('adding file'),
1894
 
                                    num + len(self._removed_id), total_entries)
1895
 
                file_id = new_path_file_ids[trans_id]
1896
 
                if file_id is None:
1897
 
                    continue
1898
 
                kind = self.final_kind(trans_id)
1899
 
                if kind is None:
1900
 
                    kind = self._tree.stored_kind(self._tree.id2path(file_id))
1901
 
                parent_trans_id = self.final_parent(trans_id)
1902
 
                parent_file_id = new_path_file_ids.get(parent_trans_id)
1903
 
                if parent_file_id is None:
1904
 
                    parent_file_id = self.final_file_id(parent_trans_id)
1905
 
                if trans_id in self._new_reference_revision:
1906
 
                    new_entry = inventory.TreeReference(
1907
 
                        file_id,
1908
 
                        self._new_name[trans_id],
1909
 
                        self.final_file_id(self._new_parent[trans_id]),
1910
 
                        None, self._new_reference_revision[trans_id])
1911
 
                else:
1912
 
                    new_entry = inventory.make_entry(kind,
1913
 
                                                     self.final_name(trans_id),
1914
 
                                                     parent_file_id, file_id)
1915
 
                try:
1916
 
                    old_path = self._tree.id2path(new_entry.file_id)
1917
 
                except errors.NoSuchId:
1918
 
                    old_path = None
1919
 
                new_executability = self._new_executability.get(trans_id)
1920
 
                if new_executability is not None:
1921
 
                    new_entry.executable = new_executability
1922
 
                inventory_delta.append(
1923
 
                    (old_path, path, new_entry.file_id, new_entry))
1924
 
        return inventory_delta
1925
 
 
1926
 
    def _apply_removals(self, mover):
1927
 
        """Perform tree operations that remove directory/inventory names.
1928
 
 
1929
 
        That is, delete files that are to be deleted, and put any files that
1930
 
        need renaming into limbo.  This must be done in strict child-to-parent
1931
 
        order.
1932
 
 
1933
 
        If inventory_delta is None, no inventory delta generation is performed.
1934
 
        """
1935
 
        tree_paths = sorted(viewitems(self._tree_path_ids), reverse=True)
1936
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1937
 
            for num, (path, trans_id) in enumerate(tree_paths):
1938
 
                # do not attempt to move root into a subdirectory of itself.
1939
 
                if path == '':
1940
 
                    continue
1941
 
                child_pb.update(gettext('removing file'), num, len(tree_paths))
1942
 
                full_path = self._tree.abspath(path)
1943
 
                if trans_id in self._removed_contents:
1944
 
                    delete_path = os.path.join(self._deletiondir, trans_id)
1945
 
                    mover.pre_delete(full_path, delete_path)
1946
 
                elif (trans_id in self._new_name or
1947
 
                      trans_id in self._new_parent):
1948
 
                    try:
1949
 
                        mover.rename(full_path, self._limbo_name(trans_id))
1950
 
                    except errors.TransformRenameFailed as e:
1951
 
                        if e.errno != errno.ENOENT:
1952
 
                            raise
1953
 
                    else:
1954
 
                        self.rename_count += 1
1955
 
 
1956
 
    def _apply_insertions(self, mover):
1957
 
        """Perform tree operations that insert directory/inventory names.
1958
 
 
1959
 
        That is, create any files that need to be created, and restore from
1960
 
        limbo any files that needed renaming.  This must be done in strict
1961
 
        parent-to-child order.
1962
 
 
1963
 
        If inventory_delta is None, no inventory delta is calculated, and
1964
 
        no list of modified paths is returned.
1965
 
        """
1966
 
        new_paths = self.new_paths(filesystem_only=True)
1967
 
        modified_paths = []
1968
 
        with ui.ui_factory.nested_progress_bar() as child_pb:
1969
 
            for num, (path, trans_id) in enumerate(new_paths):
1970
 
                if (num % 10) == 0:
1971
 
                    child_pb.update(gettext('adding file'),
1972
 
                                    num, len(new_paths))
1973
 
                full_path = self._tree.abspath(path)
1974
 
                if trans_id in self._needs_rename:
1975
 
                    try:
1976
 
                        mover.rename(self._limbo_name(trans_id), full_path)
1977
 
                    except errors.TransformRenameFailed as e:
1978
 
                        # We may be renaming a dangling inventory id
1979
 
                        if e.errno != errno.ENOENT:
1980
 
                            raise
1981
 
                    else:
1982
 
                        self.rename_count += 1
1983
 
                    # TODO: if trans_id in self._observed_sha1s, we should
1984
 
                    #       re-stat the final target, since ctime will be
1985
 
                    #       updated by the change.
1986
 
                if (trans_id in self._new_contents
1987
 
                        or self.path_changed(trans_id)):
1988
 
                    if trans_id in self._new_contents:
1989
 
                        modified_paths.append(full_path)
1990
 
                if trans_id in self._new_executability:
1991
 
                    self._set_executability(path, trans_id)
1992
 
                if trans_id in self._observed_sha1s:
1993
 
                    o_sha1, o_st_val = self._observed_sha1s[trans_id]
1994
 
                    st = osutils.lstat(full_path)
1995
 
                    self._observed_sha1s[trans_id] = (o_sha1, st)
1996
 
        for path, trans_id in new_paths:
1997
 
            # new_paths includes stuff like workingtree conflicts. Only the
1998
 
            # stuff in new_contents actually comes from limbo.
1999
 
            if trans_id in self._limbo_files:
2000
 
                del self._limbo_files[trans_id]
2001
 
        self._new_contents.clear()
2002
 
        return modified_paths
2003
 
 
2004
 
    def _apply_observed_sha1s(self):
2005
 
        """After we have finished renaming everything, update observed sha1s
2006
 
 
2007
 
        This has to be done after self._tree.apply_inventory_delta, otherwise
2008
 
        it doesn't know anything about the files we are updating. Also, we want
2009
 
        to do this as late as possible, so that most entries end up cached.
2010
 
        """
2011
 
        # TODO: this doesn't update the stat information for directories. So
2012
 
        #       the first 'bzr status' will still need to rewrite
2013
 
        #       .bzr/checkout/dirstate. However, we at least don't need to
2014
 
        #       re-read all of the files.
2015
 
        # TODO: If the operation took a while, we could do a time.sleep(3) here
2016
 
        #       to allow the clock to tick over and ensure we won't have any
2017
 
        #       problems. (we could observe start time, and finish time, and if
2018
 
        #       it is less than eg 10% overhead, add a sleep call.)
2019
 
        paths = FinalPaths(self)
2020
 
        for trans_id, observed in viewitems(self._observed_sha1s):
2021
 
            path = paths.get_path(trans_id)
2022
 
            self._tree._observed_sha1(path, observed)
2023
 
 
2024
 
 
2025
 
class TransformPreview(DiskTreeTransform):
2026
 
    """A TreeTransform for generating preview trees.
2027
 
 
2028
 
    Unlike TreeTransform, this version works when the input tree is a
2029
 
    RevisionTree, rather than a WorkingTree.  As a result, it tends to ignore
2030
 
    unversioned files in the input tree.
2031
 
    """
2032
 
 
2033
 
    def __init__(self, tree, pb=None, case_sensitive=True):
2034
 
        tree.lock_read()
2035
 
        limbodir = osutils.mkdtemp(prefix='bzr-limbo-')
2036
 
        DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive)
2037
 
 
2038
 
    def tree_kind(self, trans_id):
2039
 
        path = self._tree_id_paths.get(trans_id)
2040
 
        if path is None:
2041
 
            return None
2042
 
        kind = self._tree.path_content_summary(path)[0]
2043
 
        if kind == 'missing':
2044
 
            kind = None
2045
 
        return kind
2046
 
 
2047
 
    def _set_mode(self, trans_id, mode_id, typefunc):
2048
 
        """Set the mode of new file contents.
2049
 
        The mode_id is the existing file to get the mode from (often the same
2050
 
        as trans_id).  The operation is only performed if there's a mode match
2051
 
        according to typefunc.
2052
 
        """
2053
 
        # is it ok to ignore this?  probably
2054
 
        pass
2055
 
 
2056
 
    def iter_tree_children(self, parent_id):
2057
 
        """Iterate through the entry's tree children, if any"""
2058
 
        try:
2059
 
            path = self._tree_id_paths[parent_id]
2060
 
        except KeyError:
2061
 
            return
2062
 
        try:
2063
 
            entry = next(self._tree.iter_entries_by_dir(
2064
 
                specific_files=[path]))[1]
2065
 
        except StopIteration:
2066
 
            return
2067
 
        children = getattr(entry, 'children', {})
2068
 
        for child in children:
2069
 
            childpath = joinpath(path, child)
2070
 
            yield self.trans_id_tree_path(childpath)
2071
 
 
2072
 
    def new_orphan(self, trans_id, parent_id):
2073
 
        raise NotImplementedError(self.new_orphan)
2074
 
 
2075
 
 
2076
 
class _PreviewTree(inventorytree.InventoryTree):
2077
 
    """Partial implementation of Tree to support show_diff_trees"""
2078
 
 
2079
 
    def __init__(self, transform):
2080
 
        self._transform = transform
2081
 
        self._final_paths = FinalPaths(transform)
2082
 
        self.__by_parent = None
2083
 
        self._parent_ids = []
2084
 
        self._all_children_cache = {}
2085
 
        self._path2trans_id_cache = {}
2086
 
        self._final_name_cache = {}
2087
 
        self._iter_changes_cache = dict((c.file_id, c) for c in
2088
 
                                        self._transform.iter_changes())
2089
 
 
2090
 
    def supports_tree_reference(self):
2091
 
        # TODO(jelmer): Support tree references in _PreviewTree.
2092
 
        # return self._transform._tree.supports_tree_reference()
2093
 
        return False
2094
 
 
2095
 
    def _content_change(self, file_id):
2096
 
        """Return True if the content of this file changed"""
2097
 
        changes = self._iter_changes_cache.get(file_id)
2098
 
        return (changes is not None and changes.changed_content)
2099
 
 
2100
 
    def _get_repository(self):
2101
 
        repo = getattr(self._transform._tree, '_repository', None)
2102
 
        if repo is None:
2103
 
            repo = self._transform._tree.branch.repository
2104
 
        return repo
2105
 
 
2106
 
    def _iter_parent_trees(self):
2107
 
        for revision_id in self.get_parent_ids():
2108
 
            try:
2109
 
                yield self.revision_tree(revision_id)
2110
 
            except errors.NoSuchRevisionInTree:
2111
 
                yield self._get_repository().revision_tree(revision_id)
2112
 
 
2113
 
    def _get_file_revision(self, path, file_id, vf, tree_revision):
2114
 
        parent_keys = [
2115
 
            (file_id, t.get_file_revision(t.id2path(file_id)))
2116
 
            for t in self._iter_parent_trees()]
2117
 
        vf.add_lines((file_id, tree_revision), parent_keys,
2118
 
                     self.get_file_lines(path))
2119
 
        repo = self._get_repository()
2120
 
        base_vf = repo.texts
2121
 
        if base_vf not in vf.fallback_versionedfiles:
2122
 
            vf.fallback_versionedfiles.append(base_vf)
2123
 
        return tree_revision
2124
 
 
2125
 
    def _stat_limbo_file(self, trans_id):
2126
 
        name = self._transform._limbo_name(trans_id)
2127
 
        return os.lstat(name)
2128
 
 
2129
 
    @property
2130
 
    def _by_parent(self):
2131
 
        if self.__by_parent is None:
2132
 
            self.__by_parent = self._transform.by_parent()
2133
 
        return self.__by_parent
2134
 
 
2135
 
    def _comparison_data(self, entry, path):
2136
 
        kind, size, executable, link_or_sha1 = self.path_content_summary(path)
2137
 
        if kind == 'missing':
2138
 
            kind = None
2139
 
            executable = False
2140
 
        else:
2141
 
            file_id = self._transform.final_file_id(self._path2trans_id(path))
2142
 
            executable = self.is_executable(path)
2143
 
        return kind, executable, None
2144
 
 
2145
 
    def is_locked(self):
2146
 
        return False
2147
 
 
2148
 
    def lock_read(self):
2149
 
        # Perhaps in theory, this should lock the TreeTransform?
2150
 
        return lock.LogicalLockResult(self.unlock)
2151
 
 
2152
 
    def unlock(self):
2153
 
        pass
2154
 
 
2155
 
    @property
2156
 
    def root_inventory(self):
2157
 
        """This Tree does not use inventory as its backing data."""
2158
 
        raise NotImplementedError(_PreviewTree.root_inventory)
2159
 
 
2160
 
    def all_file_ids(self):
2161
 
        tree_ids = set(self._transform._tree.all_file_ids())
2162
 
        tree_ids.difference_update(self._transform.tree_file_id(t)
2163
 
                                   for t in self._transform._removed_id)
2164
 
        tree_ids.update(viewvalues(self._transform._new_id))
2165
 
        return tree_ids
2166
 
 
2167
 
    def all_versioned_paths(self):
2168
 
        tree_paths = set(self._transform._tree.all_versioned_paths())
2169
 
 
2170
 
        tree_paths.difference_update(
2171
 
            self._transform.trans_id_tree_path(t)
2172
 
            for t in self._transform._removed_id)
2173
 
 
2174
 
        tree_paths.update(
2175
 
            self._final_paths._determine_path(t)
2176
 
            for t in self._transform._new_id)
2177
 
 
2178
 
        return tree_paths
2179
 
 
2180
 
    def _path2trans_id(self, path):
2181
 
        # We must not use None here, because that is a valid value to store.
2182
 
        trans_id = self._path2trans_id_cache.get(path, object)
2183
 
        if trans_id is not object:
2184
 
            return trans_id
2185
 
        segments = splitpath(path)
2186
 
        cur_parent = self._transform.root
2187
 
        for cur_segment in segments:
2188
 
            for child in self._all_children(cur_parent):
2189
 
                final_name = self._final_name_cache.get(child)
2190
 
                if final_name is None:
2191
 
                    final_name = self._transform.final_name(child)
2192
 
                    self._final_name_cache[child] = final_name
2193
 
                if final_name == cur_segment:
2194
 
                    cur_parent = child
2195
 
                    break
2196
 
            else:
2197
 
                self._path2trans_id_cache[path] = None
2198
 
                return None
2199
 
        self._path2trans_id_cache[path] = cur_parent
2200
 
        return cur_parent
2201
 
 
2202
 
    def path2id(self, path):
2203
 
        if isinstance(path, list):
2204
 
            if path == []:
2205
 
                path = [""]
2206
 
            path = osutils.pathjoin(*path)
2207
 
        return self._transform.final_file_id(self._path2trans_id(path))
2208
 
 
2209
 
    def id2path(self, file_id, recurse='down'):
2210
 
        trans_id = self._transform.trans_id_file_id(file_id)
2211
 
        try:
2212
 
            return self._final_paths._determine_path(trans_id)
2213
 
        except NoFinalPath:
2214
 
            raise errors.NoSuchId(self, file_id)
2215
 
 
2216
 
    def _all_children(self, trans_id):
2217
 
        children = self._all_children_cache.get(trans_id)
2218
 
        if children is not None:
2219
 
            return children
2220
 
        children = set(self._transform.iter_tree_children(trans_id))
2221
 
        # children in the _new_parent set are provided by _by_parent.
2222
 
        children.difference_update(self._transform._new_parent)
2223
 
        children.update(self._by_parent.get(trans_id, []))
2224
 
        self._all_children_cache[trans_id] = children
2225
 
        return children
2226
 
 
2227
 
    def extras(self):
2228
 
        possible_extras = set(self._transform.trans_id_tree_path(p) for p
2229
 
                              in self._transform._tree.extras())
2230
 
        possible_extras.update(self._transform._new_contents)
2231
 
        possible_extras.update(self._transform._removed_id)
2232
 
        for trans_id in possible_extras:
2233
 
            if self._transform.final_file_id(trans_id) is None:
2234
 
                yield self._final_paths._determine_path(trans_id)
2235
 
 
2236
 
    def _make_inv_entries(self, ordered_entries, specific_files=None):
2237
 
        for trans_id, parent_file_id in ordered_entries:
2238
 
            file_id = self._transform.final_file_id(trans_id)
2239
 
            if file_id is None:
2240
 
                continue
2241
 
            if (specific_files is not None
2242
 
                    and self._final_paths.get_path(trans_id) not in specific_files):
2243
 
                continue
2244
 
            kind = self._transform.final_kind(trans_id)
2245
 
            if kind is None:
2246
 
                kind = self._transform._tree.stored_kind(
2247
 
                    self._transform._tree.id2path(file_id))
2248
 
            new_entry = inventory.make_entry(
2249
 
                kind,
2250
 
                self._transform.final_name(trans_id),
2251
 
                parent_file_id, file_id)
2252
 
            yield new_entry, trans_id
2253
 
 
2254
 
    def _list_files_by_dir(self):
2255
 
        todo = [ROOT_PARENT]
2256
 
        ordered_ids = []
2257
 
        while len(todo) > 0:
2258
 
            parent = todo.pop()
2259
 
            parent_file_id = self._transform.final_file_id(parent)
2260
 
            children = list(self._all_children(parent))
2261
 
            paths = dict(zip(children, self._final_paths.get_paths(children)))
2262
 
            children.sort(key=paths.get)
2263
 
            todo.extend(reversed(children))
2264
 
            for trans_id in children:
2265
 
                ordered_ids.append((trans_id, parent_file_id))
2266
 
        return ordered_ids
2267
 
 
2268
 
    def iter_child_entries(self, path):
2269
 
        trans_id = self._path2trans_id(path)
2270
 
        if trans_id is None:
2271
 
            raise errors.NoSuchFile(path)
2272
 
        todo = [(child_trans_id, trans_id) for child_trans_id in
2273
 
                self._all_children(trans_id)]
2274
 
        for entry, trans_id in self._make_inv_entries(todo):
2275
 
            yield entry
2276
 
 
2277
 
    def iter_entries_by_dir(self, specific_files=None, recurse_nested=False):
2278
 
        if recurse_nested:
2279
 
            raise NotImplementedError(
2280
 
                'follow tree references not yet supported')
2281
 
 
2282
 
        # This may not be a maximally efficient implementation, but it is
2283
 
        # reasonably straightforward.  An implementation that grafts the
2284
 
        # TreeTransform changes onto the tree's iter_entries_by_dir results
2285
 
        # might be more efficient, but requires tricky inferences about stack
2286
 
        # position.
2287
 
        ordered_ids = self._list_files_by_dir()
2288
 
        for entry, trans_id in self._make_inv_entries(ordered_ids,
2289
 
                                                      specific_files):
2290
 
            yield self._final_paths.get_path(trans_id), entry
2291
 
 
2292
 
    def _iter_entries_for_dir(self, dir_path):
2293
 
        """Return path, entry for items in a directory without recursing down."""
2294
 
        ordered_ids = []
2295
 
        dir_trans_id = self._path2trans_id(dir_path)
2296
 
        dir_id = self._transform.final_file_id(dir_trans_id)
2297
 
        for child_trans_id in self._all_children(dir_trans_id):
2298
 
            ordered_ids.append((child_trans_id, dir_id))
2299
 
        path_entries = []
2300
 
        for entry, trans_id in self._make_inv_entries(ordered_ids):
2301
 
            path_entries.append((self._final_paths.get_path(trans_id), entry))
2302
 
        path_entries.sort()
2303
 
        return path_entries
2304
 
 
2305
 
    def list_files(self, include_root=False, from_dir=None, recursive=True,
2306
 
                   recurse_nested=False):
2307
 
        """See WorkingTree.list_files."""
2308
 
        if recurse_nested:
2309
 
            raise NotImplementedError(
2310
 
                'follow tree references not yet supported')
2311
 
 
2312
 
        # XXX This should behave like WorkingTree.list_files, but is really
2313
 
        # more like RevisionTree.list_files.
2314
 
        if from_dir == '.':
2315
 
            from_dir = None
2316
 
        if recursive:
2317
 
            prefix = None
2318
 
            if from_dir:
2319
 
                prefix = from_dir + '/'
2320
 
            entries = self.iter_entries_by_dir()
2321
 
            for path, entry in entries:
2322
 
                if entry.name == '' and not include_root:
2323
 
                    continue
2324
 
                if prefix:
2325
 
                    if not path.startswith(prefix):
2326
 
                        continue
2327
 
                    path = path[len(prefix):]
2328
 
                yield path, 'V', entry.kind, entry
2329
 
        else:
2330
 
            if from_dir is None and include_root is True:
2331
 
                root_entry = inventory.make_entry(
2332
 
                    'directory', '', ROOT_PARENT, self.path2id(''))
2333
 
                yield '', 'V', 'directory', root_entry
2334
 
            entries = self._iter_entries_for_dir(from_dir or '')
2335
 
            for path, entry in entries:
2336
 
                yield path, 'V', entry.kind, entry
2337
 
 
2338
 
    def kind(self, path):
2339
 
        trans_id = self._path2trans_id(path)
2340
 
        if trans_id is None:
2341
 
            raise errors.NoSuchFile(path)
2342
 
        return self._transform.final_kind(trans_id)
2343
 
 
2344
 
    def stored_kind(self, path):
2345
 
        trans_id = self._path2trans_id(path)
2346
 
        if trans_id is None:
2347
 
            raise errors.NoSuchFile(path)
2348
 
        try:
2349
 
            return self._transform._new_contents[trans_id]
2350
 
        except KeyError:
2351
 
            return self._transform._tree.stored_kind(path)
2352
 
 
2353
 
    def get_file_mtime(self, path):
2354
 
        """See Tree.get_file_mtime"""
2355
 
        file_id = self.path2id(path)
2356
 
        if file_id is None:
2357
 
            raise errors.NoSuchFile(path)
2358
 
        if not self._content_change(file_id):
2359
 
            return self._transform._tree.get_file_mtime(
2360
 
                self._transform._tree.id2path(file_id))
2361
 
        trans_id = self._path2trans_id(path)
2362
 
        return self._stat_limbo_file(trans_id).st_mtime
2363
 
 
2364
 
    def get_file_size(self, path):
2365
 
        """See Tree.get_file_size"""
2366
 
        trans_id = self._path2trans_id(path)
2367
 
        if trans_id is None:
2368
 
            raise errors.NoSuchFile(path)
2369
 
        kind = self._transform.final_kind(trans_id)
2370
 
        if kind != 'file':
2371
 
            return None
2372
 
        if trans_id in self._transform._new_contents:
2373
 
            return self._stat_limbo_file(trans_id).st_size
2374
 
        if self.kind(path) == 'file':
2375
 
            return self._transform._tree.get_file_size(path)
2376
 
        else:
2377
 
            return None
2378
 
 
2379
 
    def get_file_verifier(self, path, stat_value=None):
2380
 
        trans_id = self._path2trans_id(path)
2381
 
        if trans_id is None:
2382
 
            raise errors.NoSuchFile(path)
2383
 
        kind = self._transform._new_contents.get(trans_id)
2384
 
        if kind is None:
2385
 
            return self._transform._tree.get_file_verifier(path)
2386
 
        if kind == 'file':
2387
 
            with self.get_file(path) as fileobj:
2388
 
                return ("SHA1", sha_file(fileobj))
2389
 
 
2390
 
    def get_file_sha1(self, path, stat_value=None):
2391
 
        trans_id = self._path2trans_id(path)
2392
 
        if trans_id is None:
2393
 
            raise errors.NoSuchFile(path)
2394
 
        kind = self._transform._new_contents.get(trans_id)
2395
 
        if kind is None:
2396
 
            return self._transform._tree.get_file_sha1(path)
2397
 
        if kind == 'file':
2398
 
            with self.get_file(path) as fileobj:
2399
 
                return sha_file(fileobj)
2400
 
 
2401
 
    def get_reference_revision(self, path):
2402
 
        trans_id = self._path2trans_id(path)
2403
 
        if trans_id is None:
2404
 
            raise errors.NoSuchFile(path)
2405
 
        reference_revision = self._transform._new_reference_revision.get(trans_id)
2406
 
        if reference_revision is None:
2407
 
            return self._transform._tree.get_reference_revision(path)
2408
 
        return reference_revision
2409
 
 
2410
 
    def is_executable(self, path):
2411
 
        trans_id = self._path2trans_id(path)
2412
 
        if trans_id is None:
2413
 
            return False
2414
 
        try:
2415
 
            return self._transform._new_executability[trans_id]
2416
 
        except KeyError:
2417
 
            try:
2418
 
                return self._transform._tree.is_executable(path)
2419
 
            except OSError as e:
2420
 
                if e.errno == errno.ENOENT:
2421
 
                    return False
2422
 
                raise
2423
 
            except errors.NoSuchFile:
2424
 
                return False
2425
 
 
2426
 
    def has_filename(self, path):
2427
 
        trans_id = self._path2trans_id(path)
2428
 
        if trans_id in self._transform._new_contents:
2429
 
            return True
2430
 
        elif trans_id in self._transform._removed_contents:
2431
 
            return False
2432
 
        else:
2433
 
            return self._transform._tree.has_filename(path)
2434
 
 
2435
 
    def path_content_summary(self, path):
2436
 
        trans_id = self._path2trans_id(path)
2437
 
        tt = self._transform
2438
 
        tree_path = tt._tree_id_paths.get(trans_id)
2439
 
        kind = tt._new_contents.get(trans_id)
2440
 
        if kind is None:
2441
 
            if tree_path is None or trans_id in tt._removed_contents:
2442
 
                return 'missing', None, None, None
2443
 
            summary = tt._tree.path_content_summary(tree_path)
2444
 
            kind, size, executable, link_or_sha1 = summary
2445
 
        else:
2446
 
            link_or_sha1 = None
2447
 
            limbo_name = tt._limbo_name(trans_id)
2448
 
            if trans_id in tt._new_reference_revision:
2449
 
                kind = 'tree-reference'
2450
 
            if kind == 'file':
2451
 
                statval = os.lstat(limbo_name)
2452
 
                size = statval.st_size
2453
 
                if not tt._limbo_supports_executable():
2454
 
                    executable = False
2455
 
                else:
2456
 
                    executable = statval.st_mode & S_IEXEC
2457
 
            else:
2458
 
                size = None
2459
 
                executable = None
2460
 
            if kind == 'symlink':
2461
 
                link_or_sha1 = os.readlink(limbo_name)
2462
 
                if not isinstance(link_or_sha1, text_type):
2463
 
                    link_or_sha1 = link_or_sha1.decode(osutils._fs_enc)
2464
 
        executable = tt._new_executability.get(trans_id, executable)
2465
 
        return kind, size, executable, link_or_sha1
2466
 
 
2467
 
    def iter_changes(self, from_tree, include_unchanged=False,
2468
 
                     specific_files=None, pb=None, extra_trees=None,
2469
 
                     require_versioned=True, want_unversioned=False):
2470
 
        """See InterTree.iter_changes.
2471
 
 
2472
 
        This has a fast path that is only used when the from_tree matches
2473
 
        the transform tree, and no fancy options are supplied.
2474
 
        """
2475
 
        if (from_tree is not self._transform._tree or include_unchanged
2476
 
                or specific_files or want_unversioned):
2477
 
            from .bzr.inventorytree import InterInventoryTree
2478
 
            return InterInventoryTree(from_tree, self).iter_changes(
2479
 
                include_unchanged=include_unchanged,
2480
 
                specific_files=specific_files,
2481
 
                pb=pb,
2482
 
                extra_trees=extra_trees,
2483
 
                require_versioned=require_versioned,
2484
 
                want_unversioned=want_unversioned)
2485
 
        if want_unversioned:
2486
 
            raise ValueError('want_unversioned is not supported')
2487
 
        return self._transform.iter_changes()
2488
 
 
2489
 
    def get_file(self, path):
2490
 
        """See Tree.get_file"""
2491
 
        file_id = self.path2id(path)
2492
 
        if not self._content_change(file_id):
2493
 
            return self._transform._tree.get_file(path)
2494
 
        trans_id = self._path2trans_id(path)
2495
 
        name = self._transform._limbo_name(trans_id)
2496
 
        return open(name, 'rb')
2497
 
 
2498
 
    def get_file_with_stat(self, path):
2499
 
        return self.get_file(path), None
2500
 
 
2501
 
    def annotate_iter(self, path,
2502
 
                      default_revision=_mod_revision.CURRENT_REVISION):
2503
 
        file_id = self.path2id(path)
2504
 
        changes = self._iter_changes_cache.get(file_id)
2505
 
        if changes is None:
2506
 
            get_old = True
2507
 
        else:
2508
 
            changed_content, versioned, kind = (
2509
 
                changes.changed_content, changes.versioned, changes.kind)
2510
 
            if kind[1] is None:
2511
 
                return None
2512
 
            get_old = (kind[0] == 'file' and versioned[0])
2513
 
        if get_old:
2514
 
            old_annotation = self._transform._tree.annotate_iter(
2515
 
                path, default_revision=default_revision)
2516
 
        else:
2517
 
            old_annotation = []
2518
 
        if changes is None:
2519
 
            return old_annotation
2520
 
        if not changed_content:
2521
 
            return old_annotation
2522
 
        # TODO: This is doing something similar to what WT.annotate_iter is
2523
 
        #       doing, however it fails slightly because it doesn't know what
2524
 
        #       the *other* revision_id is, so it doesn't know how to give the
2525
 
        #       other as the origin for some lines, they all get
2526
 
        #       'default_revision'
2527
 
        #       It would be nice to be able to use the new Annotator based
2528
 
        #       approach, as well.
2529
 
        return annotate.reannotate([old_annotation],
2530
 
                                   self.get_file(path).readlines(),
2531
 
                                   default_revision)
2532
 
 
2533
 
    def get_symlink_target(self, path):
2534
 
        """See Tree.get_symlink_target"""
2535
 
        file_id = self.path2id(path)
2536
 
        if not self._content_change(file_id):
2537
 
            return self._transform._tree.get_symlink_target(path)
2538
 
        trans_id = self._path2trans_id(path)
2539
 
        name = self._transform._limbo_name(trans_id)
2540
 
        return osutils.readlink(name)
2541
 
 
2542
 
    def walkdirs(self, prefix=''):
2543
 
        pending = [self._transform.root]
2544
 
        while len(pending) > 0:
2545
 
            parent_id = pending.pop()
2546
 
            children = []
2547
 
            subdirs = []
2548
 
            prefix = prefix.rstrip('/')
2549
 
            parent_path = self._final_paths.get_path(parent_id)
2550
 
            parent_file_id = self._transform.final_file_id(parent_id)
2551
 
            for child_id in self._all_children(parent_id):
2552
 
                path_from_root = self._final_paths.get_path(child_id)
2553
 
                basename = self._transform.final_name(child_id)
2554
 
                file_id = self._transform.final_file_id(child_id)
2555
 
                kind = self._transform.final_kind(child_id)
2556
 
                if kind is not None:
2557
 
                    versioned_kind = kind
2558
 
                else:
2559
 
                    kind = 'unknown'
2560
 
                    versioned_kind = self._transform._tree.stored_kind(
2561
 
                        self._transform._tree.id2path(file_id))
2562
 
                if versioned_kind == 'directory':
2563
 
                    subdirs.append(child_id)
2564
 
                children.append((path_from_root, basename, kind, None,
2565
 
                                 file_id, versioned_kind))
2566
 
            children.sort()
2567
 
            if parent_path.startswith(prefix):
2568
 
                yield (parent_path, parent_file_id), children
2569
 
            pending.extend(sorted(subdirs, key=self._final_paths.get_path,
2570
 
                                  reverse=True))
2571
 
 
2572
 
    def get_parent_ids(self):
2573
 
        return self._parent_ids
2574
 
 
2575
 
    def set_parent_ids(self, parent_ids):
2576
 
        self._parent_ids = parent_ids
2577
 
 
2578
 
    def get_revision_tree(self, revision_id):
2579
 
        return self._transform._tree.get_revision_tree(revision_id)
2580
 
 
2581
 
 
2582
602
def joinpath(parent, child):
2583
603
    """Join tree-relative paths, handling the tree root specially"""
2584
604
    if parent is None or parent == "":
2645
665
    :param delta_from_tree: If true, build_tree may use the input Tree to
2646
666
        generate the inventory delta.
2647
667
    """
2648
 
    with cleanup.ExitStack() as exit_stack:
 
668
    with contextlib.ExitStack() as exit_stack:
2649
669
        exit_stack.enter_context(wt.lock_tree_write())
2650
670
        exit_stack.enter_context(tree.lock_read())
2651
671
        if accelerator_tree is not None:
2677
697
    divert = set()
2678
698
    try:
2679
699
        pp.next_phase()
2680
 
        file_trans_id[wt.path2id('')] = tt.trans_id_tree_path('')
 
700
        file_trans_id[find_previous_path(wt, tree, '')] = tt.trans_id_tree_path('')
2681
701
        with ui.ui_factory.nested_progress_bar() as pb:
2682
702
            deferred_contents = []
2683
703
            num = 0
2714
734
                        except errors.NotBranchError:
2715
735
                            pass
2716
736
                        else:
2717
 
                            divert.add(file_id)
2718
 
                    if (file_id not in divert
 
737
                            divert.add(tree_path)
 
738
                    if (tree_path not in divert
2719
739
                        and _content_match(
2720
740
                            tree, entry, tree_path, kind, target_path)):
2721
741
                        tt.delete_contents(tt.trans_id_tree_path(tree_path))
2722
742
                        if kind == 'directory':
2723
743
                            reparent = True
2724
 
                parent_id = file_trans_id[entry.parent_id]
 
744
                parent_id = file_trans_id[osutils.dirname(tree_path)]
2725
745
                if entry.kind == 'file':
2726
746
                    # We *almost* replicate new_by_entry, so that we can defer
2727
747
                    # getting the file text, and get them all at once.
2728
748
                    trans_id = tt.create_path(entry.name, parent_id)
2729
 
                    file_trans_id[file_id] = trans_id
 
749
                    file_trans_id[tree_path] = trans_id
2730
750
                    tt.version_file(trans_id, file_id=file_id)
2731
751
                    executable = tree.is_executable(tree_path)
2732
752
                    if executable:
2734
754
                    trans_data = (trans_id, tree_path, entry.text_sha1)
2735
755
                    deferred_contents.append((tree_path, trans_data))
2736
756
                else:
2737
 
                    file_trans_id[file_id] = new_by_entry(
 
757
                    file_trans_id[tree_path] = new_by_entry(
2738
758
                        tree_path, tt, entry, parent_id, tree)
2739
759
                if reparent:
2740
 
                    new_trans_id = file_trans_id[file_id]
 
760
                    new_trans_id = file_trans_id[tree_path]
2741
761
                    old_parent = tt.trans_id_tree_path(tree_path)
2742
762
                    _reparent_children(tt, old_parent, new_trans_id)
2743
763
            offset = num + 1 - len(deferred_contents)
2751
771
        raw_conflicts = resolve_conflicts(tt, pass_func=resolver)
2752
772
        if len(raw_conflicts) > 0:
2753
773
            precomputed_delta = None
2754
 
        conflicts = cook_conflicts(raw_conflicts, tt)
 
774
        conflicts = tt.cook_conflicts(raw_conflicts)
2755
775
        for conflict in conflicts:
2756
 
            trace.warning(text_type(conflict))
 
776
            trace.warning(str(conflict))
2757
777
        try:
2758
778
            wt.add_conflicts(conflicts)
2759
779
        except errors.UnsupportedOperation:
2948
968
                    unversioned_filter=working_tree.is_ignored)
2949
969
                delta.report_changes(tt.iter_changes(), change_reporter)
2950
970
            for conflict in conflicts:
2951
 
                trace.warning(text_type(conflict))
 
971
                trace.warning(str(conflict))
2952
972
            pp.next_phase()
2953
973
            tt.apply()
2954
974
            if working_tree.supports_merge_modified():
2970
990
    with ui.ui_factory.nested_progress_bar() as child_pb:
2971
991
        raw_conflicts = resolve_conflicts(
2972
992
            tt, child_pb, lambda t, c: conflict_pass(t, c, target_tree))
2973
 
    conflicts = cook_conflicts(raw_conflicts, tt)
 
993
    conflicts = tt.cook_conflicts(raw_conflicts)
2974
994
    return conflicts, merge_modified
2975
995
 
2976
996
 
2991
1011
    try:
2992
1012
        deferred_files = []
2993
1013
        for id_num, change in enumerate(change_list):
2994
 
            file_id = change.file_id
2995
1014
            target_path, wt_path = change.path
2996
1015
            target_versioned, wt_versioned = change.versioned
2997
 
            target_parent, wt_parent = change.parent_id
 
1016
            target_parent = change.parent_id[0]
2998
1017
            target_name, wt_name = change.name
2999
1018
            target_kind, wt_kind = change.kind
3000
1019
            target_executable, wt_executable = change.executable
3001
 
            if skip_root and wt_parent is None:
 
1020
            if skip_root and wt_path == '':
3002
1021
                continue
3003
 
            trans_id = tt.trans_id_file_id(file_id)
 
1022
            trans_id = tt.trans_id_file_id(change.file_id)
3004
1023
            mode_id = None
3005
1024
            if change.changed_content:
3006
1025
                keep_content = False
3025
1044
                    if not keep_content:
3026
1045
                        tt.delete_contents(trans_id)
3027
1046
                    elif target_kind is not None:
3028
 
                        parent_trans_id = tt.trans_id_file_id(wt_parent)
 
1047
                        parent_trans_id = tt.trans_id_tree_path(osutils.dirname(wt_path))
3029
1048
                        backup_name = tt._available_backup_name(
3030
1049
                            wt_name, parent_trans_id)
3031
1050
                        tt.adjust_path(backup_name, parent_trans_id, trans_id)
3032
1051
                        new_trans_id = tt.create_path(wt_name, parent_trans_id)
3033
1052
                        if wt_versioned and target_versioned:
3034
1053
                            tt.unversion_file(trans_id)
3035
 
                            tt.version_file(new_trans_id, file_id=file_id)
 
1054
                            tt.version_file(
 
1055
                                new_trans_id, file_id=getattr(change, 'file_id', None))
3036
1056
                        # New contents should have the same unix perms as old
3037
1057
                        # contents
3038
1058
                        mode_id = trans_id
3048
1068
                        target_path), trans_id)
3049
1069
                elif target_kind == 'file':
3050
1070
                    deferred_files.append(
3051
 
                        (target_path, (trans_id, mode_id, file_id)))
 
1071
                        (target_path, (trans_id, mode_id, target_path)))
3052
1072
                    if basis_tree is None:
3053
1073
                        basis_tree = working_tree.basis_tree()
3054
1074
                        basis_tree.lock_read()
3070
1090
                elif target_kind is not None:
3071
1091
                    raise AssertionError(target_kind)
3072
1092
            if not wt_versioned and target_versioned:
3073
 
                tt.version_file(trans_id, file_id=file_id)
 
1093
                tt.version_file(
 
1094
                    trans_id, file_id=getattr(change, 'file_id', None))
3074
1095
            if wt_versioned and not target_versioned:
3075
1096
                tt.unversion_file(trans_id)
3076
1097
            if (target_name is not None
3077
 
                    and (wt_name != target_name or wt_parent != target_parent)):
3078
 
                if target_name == '' and target_parent is None:
 
1098
                    and (wt_name != target_name or change.is_reparented())):
 
1099
                if target_path == '':
3079
1100
                    parent_trans = ROOT_PARENT
3080
1101
                else:
3081
1102
                    parent_trans = tt.trans_id_file_id(target_parent)
3082
 
                if wt_parent is None and wt_versioned:
 
1103
                if wt_path == '' and wt_versioned:
3083
1104
                    tt.adjust_root_path(target_name, parent_trans)
3084
1105
                else:
3085
1106
                    tt.adjust_path(target_name, parent_trans, trans_id)
3086
1107
            if wt_executable != target_executable and target_kind == "file":
3087
1108
                tt.set_executability(target_executable, trans_id)
3088
1109
        if working_tree.supports_content_filtering():
3089
 
            for (trans_id, mode_id, file_id), bytes in (
 
1110
            for (trans_id, mode_id, target_path), bytes in (
3090
1111
                    target_tree.iter_files_bytes(deferred_files)):
3091
1112
                # We're reverting a tree to the target tree so using the
3092
1113
                # target tree to find the file path seems the best choice
3093
1114
                # here IMO - Ian C 27/Oct/2009
3094
 
                filter_tree_path = target_tree.id2path(file_id)
3095
 
                filters = working_tree._content_filter_stack(filter_tree_path)
 
1115
                filters = working_tree._content_filter_stack(target_path)
3096
1116
                bytes = filtered_output_bytes(
3097
1117
                    bytes, filters,
3098
 
                    ContentFilterContext(filter_tree_path, working_tree))
 
1118
                    ContentFilterContext(target_path, working_tree))
3099
1119
                tt.create_file(bytes, trans_id, mode_id)
3100
1120
        else:
3101
 
            for (trans_id, mode_id, file_id), bytes in target_tree.iter_files_bytes(
 
1121
            for (trans_id, mode_id, target_path), bytes in target_tree.iter_files_bytes(
3102
1122
                    deferred_files):
3103
1123
                tt.create_file(bytes, trans_id, mode_id)
3104
1124
        tt.fixup_new_roots()
3116
1136
    with ui.ui_factory.nested_progress_bar() as pb:
3117
1137
        for n in range(10):
3118
1138
            pb.update(gettext('Resolution pass'), n + 1, 10)
3119
 
            conflicts = tt.find_conflicts()
 
1139
            conflicts = tt.find_raw_conflicts()
3120
1140
            if len(conflicts) == 0:
3121
1141
                return new_conflicts
3122
1142
            new_conflicts.update(pass_func(tt, conflicts))
3123
1143
        raise MalformedTransform(conflicts=conflicts)
3124
1144
 
3125
1145
 
 
1146
def resolve_duplicate_id(tt, path_tree, c_type, old_trans_id, trans_id):
 
1147
    tt.unversion_file(old_trans_id)
 
1148
    yield (c_type, 'Unversioned existing file', old_trans_id, trans_id)
 
1149
 
 
1150
 
 
1151
def resolve_duplicate(tt, path_tree, c_type, last_trans_id, trans_id, name):
 
1152
    # files that were renamed take precedence
 
1153
    final_parent = tt.final_parent(last_trans_id)
 
1154
    if tt.path_changed(last_trans_id):
 
1155
        existing_file, new_file = trans_id, last_trans_id
 
1156
    else:
 
1157
        existing_file, new_file = last_trans_id, trans_id
 
1158
    new_name = tt.final_name(existing_file) + '.moved'
 
1159
    tt.adjust_path(new_name, final_parent, existing_file)
 
1160
    yield (c_type, 'Moved existing file to', existing_file, new_file)
 
1161
 
 
1162
 
 
1163
def resolve_parent_loop(tt, path_tree, c_type, cur):
 
1164
    # break the loop by undoing one of the ops that caused the loop
 
1165
    while not tt.path_changed(cur):
 
1166
        cur = tt.final_parent(cur)
 
1167
    yield (c_type, 'Cancelled move', cur, tt.final_parent(cur),)
 
1168
    tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
 
1169
 
 
1170
 
 
1171
def resolve_missing_parent(tt, path_tree, c_type, trans_id):
 
1172
    if trans_id in tt._removed_contents:
 
1173
        cancel_deletion = True
 
1174
        orphans = tt._get_potential_orphans(trans_id)
 
1175
        if orphans:
 
1176
            cancel_deletion = False
 
1177
            # All children are orphans
 
1178
            for o in orphans:
 
1179
                try:
 
1180
                    tt.new_orphan(o, trans_id)
 
1181
                except OrphaningError:
 
1182
                    # Something bad happened so we cancel the directory
 
1183
                    # deletion which will leave it in place with a
 
1184
                    # conflict. The user can deal with it from there.
 
1185
                    # Note that this also catch the case where we don't
 
1186
                    # want to create orphans and leave the directory in
 
1187
                    # place.
 
1188
                    cancel_deletion = True
 
1189
                    break
 
1190
        if cancel_deletion:
 
1191
            # Cancel the directory deletion
 
1192
            tt.cancel_deletion(trans_id)
 
1193
            yield ('deleting parent', 'Not deleting', trans_id)
 
1194
    else:
 
1195
        create = True
 
1196
        try:
 
1197
            tt.final_name(trans_id)
 
1198
        except NoFinalPath:
 
1199
            if path_tree is not None:
 
1200
                file_id = tt.final_file_id(trans_id)
 
1201
                if file_id is None:
 
1202
                    file_id = tt.inactive_file_id(trans_id)
 
1203
                _, entry = next(path_tree.iter_entries_by_dir(
 
1204
                    specific_files=[path_tree.id2path(file_id)]))
 
1205
                # special-case the other tree root (move its
 
1206
                # children to current root)
 
1207
                if entry.parent_id is None:
 
1208
                    create = False
 
1209
                    moved = _reparent_transform_children(
 
1210
                        tt, trans_id, tt.root)
 
1211
                    for child in moved:
 
1212
                        yield (c_type, 'Moved to root', child)
 
1213
                else:
 
1214
                    parent_trans_id = tt.trans_id_file_id(
 
1215
                        entry.parent_id)
 
1216
                    tt.adjust_path(entry.name, parent_trans_id,
 
1217
                                   trans_id)
 
1218
        if create:
 
1219
            tt.create_directory(trans_id)
 
1220
            yield (c_type, 'Created directory', trans_id)
 
1221
 
 
1222
 
 
1223
def resolve_unversioned_parent(tt, path_tree, c_type, trans_id):
 
1224
    file_id = tt.inactive_file_id(trans_id)
 
1225
    # special-case the other tree root (move its children instead)
 
1226
    if path_tree and path_tree.path2id('') == file_id:
 
1227
        # This is the root entry, skip it
 
1228
        return
 
1229
    tt.version_file(trans_id, file_id=file_id)
 
1230
    yield (c_type, 'Versioned directory', trans_id)
 
1231
 
 
1232
 
 
1233
def resolve_non_directory_parent(tt, path_tree, c_type, parent_id):
 
1234
    parent_parent = tt.final_parent(parent_id)
 
1235
    parent_name = tt.final_name(parent_id)
 
1236
    parent_file_id = tt.final_file_id(parent_id)
 
1237
    new_parent_id = tt.new_directory(parent_name + '.new',
 
1238
                                     parent_parent, parent_file_id)
 
1239
    _reparent_transform_children(tt, parent_id, new_parent_id)
 
1240
    if parent_file_id is not None:
 
1241
        tt.unversion_file(parent_id)
 
1242
    yield (c_type, 'Created directory', new_parent_id)
 
1243
 
 
1244
 
 
1245
def resolve_versioning_no_contents(tt, path_tree, c_type, trans_id):
 
1246
    tt.cancel_versioning(trans_id)
 
1247
    return []
 
1248
 
 
1249
 
 
1250
CONFLICT_RESOLVERS = {
 
1251
    'duplicate id': resolve_duplicate_id,
 
1252
    'duplicate': resolve_duplicate,
 
1253
    'parent loop': resolve_parent_loop,
 
1254
    'missing parent': resolve_missing_parent,
 
1255
    'unversioned parent': resolve_unversioned_parent,
 
1256
    'non-directory parent': resolve_non_directory_parent,
 
1257
    'versioning no contents': resolve_versioning_no_contents,
 
1258
}
 
1259
 
 
1260
 
3126
1261
def conflict_pass(tt, conflicts, path_tree=None):
3127
1262
    """Resolve some classes of conflicts.
3128
1263
 
3131
1266
    :param path_tree: A Tree to get supplemental paths from
3132
1267
    """
3133
1268
    new_conflicts = set()
3134
 
    for c_type, conflict in ((c[0], c) for c in conflicts):
3135
 
        if c_type == 'duplicate id':
3136
 
            tt.unversion_file(conflict[1])
3137
 
            new_conflicts.add((c_type, 'Unversioned existing file',
3138
 
                               conflict[1], conflict[2], ))
3139
 
        elif c_type == 'duplicate':
3140
 
            # files that were renamed take precedence
3141
 
            final_parent = tt.final_parent(conflict[1])
3142
 
            if tt.path_changed(conflict[1]):
3143
 
                existing_file, new_file = conflict[2], conflict[1]
3144
 
            else:
3145
 
                existing_file, new_file = conflict[1], conflict[2]
3146
 
            new_name = tt.final_name(existing_file) + '.moved'
3147
 
            tt.adjust_path(new_name, final_parent, existing_file)
3148
 
            new_conflicts.add((c_type, 'Moved existing file to',
3149
 
                               existing_file, new_file))
3150
 
        elif c_type == 'parent loop':
3151
 
            # break the loop by undoing one of the ops that caused the loop
3152
 
            cur = conflict[1]
3153
 
            while not tt.path_changed(cur):
3154
 
                cur = tt.final_parent(cur)
3155
 
            new_conflicts.add((c_type, 'Cancelled move', cur,
3156
 
                               tt.final_parent(cur),))
3157
 
            tt.adjust_path(tt.final_name(cur), tt.get_tree_parent(cur), cur)
3158
 
 
3159
 
        elif c_type == 'missing parent':
3160
 
            trans_id = conflict[1]
3161
 
            if trans_id in tt._removed_contents:
3162
 
                cancel_deletion = True
3163
 
                orphans = tt._get_potential_orphans(trans_id)
3164
 
                if orphans:
3165
 
                    cancel_deletion = False
3166
 
                    # All children are orphans
3167
 
                    for o in orphans:
3168
 
                        try:
3169
 
                            tt.new_orphan(o, trans_id)
3170
 
                        except OrphaningError:
3171
 
                            # Something bad happened so we cancel the directory
3172
 
                            # deletion which will leave it in place with a
3173
 
                            # conflict. The user can deal with it from there.
3174
 
                            # Note that this also catch the case where we don't
3175
 
                            # want to create orphans and leave the directory in
3176
 
                            # place.
3177
 
                            cancel_deletion = True
3178
 
                            break
3179
 
                if cancel_deletion:
3180
 
                    # Cancel the directory deletion
3181
 
                    tt.cancel_deletion(trans_id)
3182
 
                    new_conflicts.add(('deleting parent', 'Not deleting',
3183
 
                                       trans_id))
3184
 
            else:
3185
 
                create = True
3186
 
                try:
3187
 
                    tt.final_name(trans_id)
3188
 
                except NoFinalPath:
3189
 
                    if path_tree is not None:
3190
 
                        file_id = tt.final_file_id(trans_id)
3191
 
                        if file_id is None:
3192
 
                            file_id = tt.inactive_file_id(trans_id)
3193
 
                        _, entry = next(path_tree.iter_entries_by_dir(
3194
 
                            specific_files=[path_tree.id2path(file_id)]))
3195
 
                        # special-case the other tree root (move its
3196
 
                        # children to current root)
3197
 
                        if entry.parent_id is None:
3198
 
                            create = False
3199
 
                            moved = _reparent_transform_children(
3200
 
                                tt, trans_id, tt.root)
3201
 
                            for child in moved:
3202
 
                                new_conflicts.add((c_type, 'Moved to root',
3203
 
                                                   child))
3204
 
                        else:
3205
 
                            parent_trans_id = tt.trans_id_file_id(
3206
 
                                entry.parent_id)
3207
 
                            tt.adjust_path(entry.name, parent_trans_id,
3208
 
                                           trans_id)
3209
 
                if create:
3210
 
                    tt.create_directory(trans_id)
3211
 
                    new_conflicts.add((c_type, 'Created directory', trans_id))
3212
 
        elif c_type == 'unversioned parent':
3213
 
            file_id = tt.inactive_file_id(conflict[1])
3214
 
            # special-case the other tree root (move its children instead)
3215
 
            if path_tree and path_tree.path2id('') == file_id:
3216
 
                # This is the root entry, skip it
3217
 
                continue
3218
 
            tt.version_file(conflict[1], file_id=file_id)
3219
 
            new_conflicts.add((c_type, 'Versioned directory', conflict[1]))
3220
 
        elif c_type == 'non-directory parent':
3221
 
            parent_id = conflict[1]
3222
 
            parent_parent = tt.final_parent(parent_id)
3223
 
            parent_name = tt.final_name(parent_id)
3224
 
            parent_file_id = tt.final_file_id(parent_id)
3225
 
            new_parent_id = tt.new_directory(parent_name + '.new',
3226
 
                                             parent_parent, parent_file_id)
3227
 
            _reparent_transform_children(tt, parent_id, new_parent_id)
3228
 
            if parent_file_id is not None:
3229
 
                tt.unversion_file(parent_id)
3230
 
            new_conflicts.add((c_type, 'Created directory', new_parent_id))
3231
 
        elif c_type == 'versioning no contents':
3232
 
            tt.cancel_versioning(conflict[1])
 
1269
    for conflict in conflicts:
 
1270
        resolver = CONFLICT_RESOLVERS.get(conflict[0])
 
1271
        if resolver is None:
 
1272
            continue
 
1273
        new_conflicts.update(resolver(tt, path_tree, *conflict))
3233
1274
    return new_conflicts
3234
1275
 
3235
1276
 
3236
 
def cook_conflicts(raw_conflicts, tt):
3237
 
    """Generate a list of cooked conflicts, sorted by file path"""
3238
 
    conflict_iter = iter_cook_conflicts(raw_conflicts, tt)
3239
 
    return sorted(conflict_iter, key=conflicts.Conflict.sort_key)
3240
 
 
3241
 
 
3242
 
def iter_cook_conflicts(raw_conflicts, tt):
3243
 
    fp = FinalPaths(tt)
3244
 
    for conflict in raw_conflicts:
3245
 
        c_type = conflict[0]
3246
 
        action = conflict[1]
3247
 
        modified_path = fp.get_path(conflict[2])
3248
 
        modified_id = tt.final_file_id(conflict[2])
3249
 
        if len(conflict) == 3:
3250
 
            yield conflicts.Conflict.factory(
3251
 
                c_type, action=action, path=modified_path, file_id=modified_id)
3252
 
 
3253
 
        else:
3254
 
            conflicting_path = fp.get_path(conflict[3])
3255
 
            conflicting_id = tt.final_file_id(conflict[3])
3256
 
            yield conflicts.Conflict.factory(
3257
 
                c_type, action=action, path=modified_path,
3258
 
                file_id=modified_id,
3259
 
                conflict_path=conflicting_path,
3260
 
                conflict_file_id=conflicting_id)
3261
 
 
3262
 
 
3263
1277
class _FileMover(object):
3264
1278
    """Moves and deletes files for TreeTransform, tracking operations"""
3265
1279
 
3276
1290
                raise errors.FileExists(to, str(e))
3277
1291
            # normal OSError doesn't include filenames so it's hard to see where
3278
1292
            # the problem is, see https://bugs.launchpad.net/bzr/+bug/491763
3279
 
            raise errors.TransformRenameFailed(from_, to, str(e), e.errno)
 
1293
            raise TransformRenameFailed(from_, to, str(e), e.errno)
3280
1294
        self.past_renames.append((from_, to))
3281
1295
 
3282
1296
    def pre_delete(self, from_, to):
3295
1309
            try:
3296
1310
                os.rename(to, from_)
3297
1311
            except OSError as e:
3298
 
                raise errors.TransformRenameFailed(to, from_, str(e), e.errno)
 
1312
                raise TransformRenameFailed(to, from_, str(e), e.errno)
3299
1313
        # after rollback, don't reuse _FileMover
3300
1314
        self.past_renames = None
3301
1315
        self.pending_deletions = None
3327
1341
            tt.delete_contents(trans_id)
3328
1342
            tt.create_hardlink(source_tree.abspath(change.path[0]), trans_id)
3329
1343
        tt.apply()
 
1344
 
 
1345
 
 
1346
class PreviewTree(object):
 
1347
    """Preview tree."""
 
1348
 
 
1349
    def __init__(self, transform):
 
1350
        self._transform = transform
 
1351
        self._parent_ids = []
 
1352
        self.__by_parent = None
 
1353
        self._path2trans_id_cache = {}
 
1354
        self._all_children_cache = {}
 
1355
        self._final_name_cache = {}
 
1356
 
 
1357
    @property
 
1358
    def _by_parent(self):
 
1359
        if self.__by_parent is None:
 
1360
            self.__by_parent = self._transform.by_parent()
 
1361
        return self.__by_parent
 
1362
 
 
1363
    def get_parent_ids(self):
 
1364
        return self._parent_ids
 
1365
 
 
1366
    def set_parent_ids(self, parent_ids):
 
1367
        self._parent_ids = parent_ids
 
1368
 
 
1369
    def get_revision_tree(self, revision_id):
 
1370
        return self._transform._tree.get_revision_tree(revision_id)
 
1371
 
 
1372
    def is_locked(self):
 
1373
        return False
 
1374
 
 
1375
    def lock_read(self):
 
1376
        # Perhaps in theory, this should lock the TreeTransform?
 
1377
        return lock.LogicalLockResult(self.unlock)
 
1378
 
 
1379
    def unlock(self):
 
1380
        pass
 
1381
 
 
1382
    def _path2trans_id(self, path):
 
1383
        # We must not use None here, because that is a valid value to store.
 
1384
        trans_id = self._path2trans_id_cache.get(path, object)
 
1385
        if trans_id is not object:
 
1386
            return trans_id
 
1387
        segments = osutils.splitpath(path)
 
1388
        cur_parent = self._transform.root
 
1389
        for cur_segment in segments:
 
1390
            for child in self._all_children(cur_parent):
 
1391
                final_name = self._final_name_cache.get(child)
 
1392
                if final_name is None:
 
1393
                    final_name = self._transform.final_name(child)
 
1394
                    self._final_name_cache[child] = final_name
 
1395
                if final_name == cur_segment:
 
1396
                    cur_parent = child
 
1397
                    break
 
1398
            else:
 
1399
                self._path2trans_id_cache[path] = None
 
1400
                return None
 
1401
        self._path2trans_id_cache[path] = cur_parent
 
1402
        return cur_parent
 
1403
 
 
1404
    def _all_children(self, trans_id):
 
1405
        children = self._all_children_cache.get(trans_id)
 
1406
        if children is not None:
 
1407
            return children
 
1408
        children = set(self._transform.iter_tree_children(trans_id))
 
1409
        # children in the _new_parent set are provided by _by_parent.
 
1410
        children.difference_update(self._transform._new_parent)
 
1411
        children.update(self._by_parent.get(trans_id, []))
 
1412
        self._all_children_cache[trans_id] = children
 
1413
        return children
 
1414
 
 
1415
    def get_file_with_stat(self, path):
 
1416
        return self.get_file(path), None
 
1417
 
 
1418
    def is_executable(self, path):
 
1419
        trans_id = self._path2trans_id(path)
 
1420
        if trans_id is None:
 
1421
            return False
 
1422
        try:
 
1423
            return self._transform._new_executability[trans_id]
 
1424
        except KeyError:
 
1425
            try:
 
1426
                return self._transform._tree.is_executable(path)
 
1427
            except OSError as e:
 
1428
                if e.errno == errno.ENOENT:
 
1429
                    return False
 
1430
                raise
 
1431
            except errors.NoSuchFile:
 
1432
                return False
 
1433
 
 
1434
    def has_filename(self, path):
 
1435
        trans_id = self._path2trans_id(path)
 
1436
        if trans_id in self._transform._new_contents:
 
1437
            return True
 
1438
        elif trans_id in self._transform._removed_contents:
 
1439
            return False
 
1440
        else:
 
1441
            return self._transform._tree.has_filename(path)
 
1442
 
 
1443
    def get_file_sha1(self, path, stat_value=None):
 
1444
        trans_id = self._path2trans_id(path)
 
1445
        if trans_id is None:
 
1446
            raise errors.NoSuchFile(path)
 
1447
        kind = self._transform._new_contents.get(trans_id)
 
1448
        if kind is None:
 
1449
            return self._transform._tree.get_file_sha1(path)
 
1450
        if kind == 'file':
 
1451
            with self.get_file(path) as fileobj:
 
1452
                return osutils.sha_file(fileobj)
 
1453
 
 
1454
    def get_file_verifier(self, path, stat_value=None):
 
1455
        trans_id = self._path2trans_id(path)
 
1456
        if trans_id is None:
 
1457
            raise errors.NoSuchFile(path)
 
1458
        kind = self._transform._new_contents.get(trans_id)
 
1459
        if kind is None:
 
1460
            return self._transform._tree.get_file_verifier(path)
 
1461
        if kind == 'file':
 
1462
            with self.get_file(path) as fileobj:
 
1463
                return ("SHA1", osutils.sha_file(fileobj))
 
1464
 
 
1465
    def kind(self, path):
 
1466
        trans_id = self._path2trans_id(path)
 
1467
        if trans_id is None:
 
1468
            raise errors.NoSuchFile(path)
 
1469
        return self._transform.final_kind(trans_id)
 
1470
 
 
1471
    def stored_kind(self, path):
 
1472
        trans_id = self._path2trans_id(path)
 
1473
        if trans_id is None:
 
1474
            raise errors.NoSuchFile(path)
 
1475
        try:
 
1476
            return self._transform._new_contents[trans_id]
 
1477
        except KeyError:
 
1478
            return self._transform._tree.stored_kind(path)
 
1479
 
 
1480
    def _get_repository(self):
 
1481
        repo = getattr(self._transform._tree, '_repository', None)
 
1482
        if repo is None:
 
1483
            repo = self._transform._tree.branch.repository
 
1484
        return repo
 
1485
 
 
1486
    def _iter_parent_trees(self):
 
1487
        for revision_id in self.get_parent_ids():
 
1488
            try:
 
1489
                yield self.revision_tree(revision_id)
 
1490
            except errors.NoSuchRevisionInTree:
 
1491
                yield self._get_repository().revision_tree(revision_id)
 
1492
 
 
1493
    def get_file_size(self, path):
 
1494
        """See Tree.get_file_size"""
 
1495
        trans_id = self._path2trans_id(path)
 
1496
        if trans_id is None:
 
1497
            raise errors.NoSuchFile(path)
 
1498
        kind = self._transform.final_kind(trans_id)
 
1499
        if kind != 'file':
 
1500
            return None
 
1501
        if trans_id in self._transform._new_contents:
 
1502
            return self._stat_limbo_file(trans_id).st_size
 
1503
        if self.kind(path) == 'file':
 
1504
            return self._transform._tree.get_file_size(path)
 
1505
        else:
 
1506
            return None
 
1507
 
 
1508
    def get_reference_revision(self, path):
 
1509
        trans_id = self._path2trans_id(path)
 
1510
        if trans_id is None:
 
1511
            raise errors.NoSuchFile(path)
 
1512
        reference_revision = self._transform._new_reference_revision.get(trans_id)
 
1513
        if reference_revision is None:
 
1514
            return self._transform._tree.get_reference_revision(path)
 
1515
        return reference_revision
 
1516
 
 
1517
    def tree_kind(self, trans_id):
 
1518
        path = self._tree_id_paths.get(trans_id)
 
1519
        if path is None:
 
1520
            return None
 
1521
        kind = self._tree.path_content_summary(path)[0]
 
1522
        if kind == 'missing':
 
1523
            kind = None
 
1524
        return kind