/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/git/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:
 
1
# Copyright (C) 2006-2011 Canonical Ltd
 
2
# Copyright (C) 2020 Breezy Developers
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
17
 
 
18
from __future__ import absolute_import
 
19
 
 
20
import errno
 
21
import os
 
22
from stat import S_ISREG
 
23
import time
 
24
 
 
25
from .. import conflicts, errors, multiparent, osutils, trace, ui, urlutils
 
26
from ..i18n import gettext
 
27
from ..mutabletree import MutableTree
 
28
from ..transform import (
 
29
    TreeTransform,
 
30
    _TransformResults,
 
31
    _FileMover,
 
32
    FinalPaths,
 
33
    joinpath,
 
34
    unique_add,
 
35
    TransformRenameFailed,
 
36
    ImmortalLimbo,
 
37
    ROOT_PARENT,
 
38
    ReusingTransform,
 
39
    MalformedTransform,
 
40
    )
 
41
from ..bzr.inventorytree import InventoryTreeChange
 
42
 
 
43
from ..bzr import inventory
 
44
from ..bzr.transform import TransformPreview as GitTransformPreview
 
45
 
 
46
 
 
47
class TreeTransformBase(TreeTransform):
 
48
    """The base class for TreeTransform and its kin."""
 
49
 
 
50
    def __init__(self, tree, pb=None, case_sensitive=True):
 
51
        """Constructor.
 
52
 
 
53
        :param tree: The tree that will be transformed, but not necessarily
 
54
            the output tree.
 
55
        :param pb: ignored
 
56
        :param case_sensitive: If True, the target of the transform is
 
57
            case sensitive, not just case preserving.
 
58
        """
 
59
        super(TreeTransformBase, self).__init__(tree, pb=pb)
 
60
        # mapping of trans_id => (sha1 of content, stat_value)
 
61
        self._observed_sha1s = {}
 
62
        # Mapping of trans_id -> new file_id
 
63
        self._new_id = {}
 
64
        # Mapping of old file-id -> trans_id
 
65
        self._non_present_ids = {}
 
66
        # Mapping of new file_id -> trans_id
 
67
        self._r_new_id = {}
 
68
        # The trans_id that will be used as the tree root
 
69
        if tree.is_versioned(''):
 
70
            self._new_root = self.trans_id_tree_path('')
 
71
        else:
 
72
            self._new_root = None
 
73
        # Whether the target is case sensitive
 
74
        self._case_sensitive_target = case_sensitive
 
75
 
 
76
    def finalize(self):
 
77
        """Release the working tree lock, if held.
 
78
 
 
79
        This is required if apply has not been invoked, but can be invoked
 
80
        even after apply.
 
81
        """
 
82
        if self._tree is None:
 
83
            return
 
84
        for hook in MutableTree.hooks['post_transform']:
 
85
            hook(self._tree, self)
 
86
        self._tree.unlock()
 
87
        self._tree = None
 
88
 
 
89
    def __get_root(self):
 
90
        return self._new_root
 
91
 
 
92
    root = property(__get_root)
 
93
 
 
94
    def create_path(self, name, parent):
 
95
        """Assign a transaction id to a new path"""
 
96
        trans_id = self._assign_id()
 
97
        unique_add(self._new_name, trans_id, name)
 
98
        unique_add(self._new_parent, trans_id, parent)
 
99
        return trans_id
 
100
 
 
101
    def adjust_root_path(self, name, parent):
 
102
        """Emulate moving the root by moving all children, instead.
 
103
 
 
104
        We do this by undoing the association of root's transaction id with the
 
105
        current tree.  This allows us to create a new directory with that
 
106
        transaction id.  We unversion the root directory and version the
 
107
        physically new directory, and hope someone versions the tree root
 
108
        later.
 
109
        """
 
110
        old_root = self._new_root
 
111
        old_root_file_id = self.final_file_id(old_root)
 
112
        # force moving all children of root
 
113
        for child_id in self.iter_tree_children(old_root):
 
114
            if child_id != parent:
 
115
                self.adjust_path(self.final_name(child_id),
 
116
                                 self.final_parent(child_id), child_id)
 
117
            file_id = self.final_file_id(child_id)
 
118
            if file_id is not None:
 
119
                self.unversion_file(child_id)
 
120
            self.version_file(child_id, file_id=file_id)
 
121
 
 
122
        # the physical root needs a new transaction id
 
123
        self._tree_path_ids.pop("")
 
124
        self._tree_id_paths.pop(old_root)
 
125
        self._new_root = self.trans_id_tree_path('')
 
126
        if parent == old_root:
 
127
            parent = self._new_root
 
128
        self.adjust_path(name, parent, old_root)
 
129
        self.create_directory(old_root)
 
130
        self.version_file(old_root, file_id=old_root_file_id)
 
131
        self.unversion_file(self._new_root)
 
132
 
 
133
    def fixup_new_roots(self):
 
134
        """Reinterpret requests to change the root directory
 
135
 
 
136
        Instead of creating a root directory, or moving an existing directory,
 
137
        all the attributes and children of the new root are applied to the
 
138
        existing root directory.
 
139
 
 
140
        This means that the old root trans-id becomes obsolete, so it is
 
141
        recommended only to invoke this after the root trans-id has become
 
142
        irrelevant.
 
143
 
 
144
        """
 
145
        new_roots = [k for k, v in self._new_parent.items()
 
146
                     if v == ROOT_PARENT]
 
147
        if len(new_roots) < 1:
 
148
            return
 
149
        if len(new_roots) != 1:
 
150
            raise ValueError('A tree cannot have two roots!')
 
151
        if self._new_root is None:
 
152
            self._new_root = new_roots[0]
 
153
            return
 
154
        old_new_root = new_roots[0]
 
155
        # unversion the new root's directory.
 
156
        if self.final_kind(self._new_root) is None:
 
157
            file_id = self.final_file_id(old_new_root)
 
158
        else:
 
159
            file_id = self.final_file_id(self._new_root)
 
160
        if old_new_root in self._new_id:
 
161
            self.cancel_versioning(old_new_root)
 
162
        else:
 
163
            self.unversion_file(old_new_root)
 
164
        # if, at this stage, root still has an old file_id, zap it so we can
 
165
        # stick a new one in.
 
166
        if (self.tree_file_id(self._new_root) is not None
 
167
                and self._new_root not in self._removed_id):
 
168
            self.unversion_file(self._new_root)
 
169
        if file_id is not None:
 
170
            self.version_file(self._new_root, file_id=file_id)
 
171
 
 
172
        # Now move children of new root into old root directory.
 
173
        # Ensure all children are registered with the transaction, but don't
 
174
        # use directly-- some tree children have new parents
 
175
        list(self.iter_tree_children(old_new_root))
 
176
        # Move all children of new root into old root directory.
 
177
        for child in self.by_parent().get(old_new_root, []):
 
178
            self.adjust_path(self.final_name(child), self._new_root, child)
 
179
 
 
180
        # Ensure old_new_root has no directory.
 
181
        if old_new_root in self._new_contents:
 
182
            self.cancel_creation(old_new_root)
 
183
        else:
 
184
            self.delete_contents(old_new_root)
 
185
 
 
186
        # prevent deletion of root directory.
 
187
        if self._new_root in self._removed_contents:
 
188
            self.cancel_deletion(self._new_root)
 
189
 
 
190
        # destroy path info for old_new_root.
 
191
        del self._new_parent[old_new_root]
 
192
        del self._new_name[old_new_root]
 
193
 
 
194
    def trans_id_file_id(self, file_id):
 
195
        """Determine or set the transaction id associated with a file ID.
 
196
        A new id is only created for file_ids that were never present.  If
 
197
        a transaction has been unversioned, it is deliberately still returned.
 
198
        (this will likely lead to an unversioned parent conflict.)
 
199
        """
 
200
        if file_id is None:
 
201
            raise ValueError('None is not a valid file id')
 
202
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
 
203
            return self._r_new_id[file_id]
 
204
        else:
 
205
            try:
 
206
                path = self._tree.id2path(file_id)
 
207
            except errors.NoSuchId:
 
208
                if file_id in self._non_present_ids:
 
209
                    return self._non_present_ids[file_id]
 
210
                else:
 
211
                    trans_id = self._assign_id()
 
212
                    self._non_present_ids[file_id] = trans_id
 
213
                    return trans_id
 
214
            else:
 
215
                return self.trans_id_tree_path(path)
 
216
 
 
217
    def version_file(self, trans_id, file_id=None):
 
218
        """Schedule a file to become versioned."""
 
219
        raise NotImplementedError(self.version_file)
 
220
 
 
221
    def cancel_versioning(self, trans_id):
 
222
        """Undo a previous versioning of a file"""
 
223
        raise NotImplementedError(self.cancel_versioning)
 
224
 
 
225
    def new_paths(self, filesystem_only=False):
 
226
        """Determine the paths of all new and changed files.
 
227
 
 
228
        :param filesystem_only: if True, only calculate values for files
 
229
            that require renames or execute bit changes.
 
230
        """
 
231
        new_ids = set()
 
232
        if filesystem_only:
 
233
            stale_ids = self._needs_rename.difference(self._new_name)
 
234
            stale_ids.difference_update(self._new_parent)
 
235
            stale_ids.difference_update(self._new_contents)
 
236
            stale_ids.difference_update(self._new_id)
 
237
            needs_rename = self._needs_rename.difference(stale_ids)
 
238
            id_sets = (needs_rename, self._new_executability)
 
239
        else:
 
240
            id_sets = (self._new_name, self._new_parent, self._new_contents,
 
241
                       self._new_id, self._new_executability)
 
242
        for id_set in id_sets:
 
243
            new_ids.update(id_set)
 
244
        return sorted(FinalPaths(self).get_paths(new_ids))
 
245
 
 
246
    def tree_file_id(self, trans_id):
 
247
        """Determine the file id associated with the trans_id in the tree"""
 
248
        path = self.tree_path(trans_id)
 
249
        if path is None:
 
250
            return None
 
251
        # the file is old; the old id is still valid
 
252
        if self._new_root == trans_id:
 
253
            return self._tree.path2id('')
 
254
        return self._tree.path2id(path)
 
255
 
 
256
    def final_is_versioned(self, trans_id):
 
257
        return self.final_file_id(trans_id) is not None
 
258
 
 
259
    def final_file_id(self, trans_id):
 
260
        """Determine the file id after any changes are applied, or None.
 
261
 
 
262
        None indicates that the file will not be versioned after changes are
 
263
        applied.
 
264
        """
 
265
        try:
 
266
            return self._new_id[trans_id]
 
267
        except KeyError:
 
268
            if trans_id in self._removed_id:
 
269
                return None
 
270
        return self.tree_file_id(trans_id)
 
271
 
 
272
    def inactive_file_id(self, trans_id):
 
273
        """Return the inactive file_id associated with a transaction id.
 
274
        That is, the one in the tree or in non_present_ids.
 
275
        The file_id may actually be active, too.
 
276
        """
 
277
        file_id = self.tree_file_id(trans_id)
 
278
        if file_id is not None:
 
279
            return file_id
 
280
        for key, value in self._non_present_ids.items():
 
281
            if value == trans_id:
 
282
                return key
 
283
 
 
284
    def find_raw_conflicts(self):
 
285
        """Find any violations of inventory or filesystem invariants"""
 
286
        if self._done is True:
 
287
            raise ReusingTransform()
 
288
        conflicts = []
 
289
        # ensure all children of all existent parents are known
 
290
        # all children of non-existent parents are known, by definition.
 
291
        self._add_tree_children()
 
292
        by_parent = self.by_parent()
 
293
        conflicts.extend(self._parent_loops())
 
294
        conflicts.extend(self._duplicate_entries(by_parent))
 
295
        conflicts.extend(self._parent_type_conflicts(by_parent))
 
296
        conflicts.extend(self._improper_versioning())
 
297
        conflicts.extend(self._executability_conflicts())
 
298
        conflicts.extend(self._overwrite_conflicts())
 
299
        return conflicts
 
300
 
 
301
    def _check_malformed(self):
 
302
        conflicts = self.find_raw_conflicts()
 
303
        if len(conflicts) != 0:
 
304
            raise MalformedTransform(conflicts=conflicts)
 
305
 
 
306
    def _add_tree_children(self):
 
307
        """Add all the children of all active parents to the known paths.
 
308
 
 
309
        Active parents are those which gain children, and those which are
 
310
        removed.  This is a necessary first step in detecting conflicts.
 
311
        """
 
312
        parents = list(self.by_parent())
 
313
        parents.extend([t for t in self._removed_contents if
 
314
                        self.tree_kind(t) == 'directory'])
 
315
        for trans_id in self._removed_id:
 
316
            path = self.tree_path(trans_id)
 
317
            if path is not None:
 
318
                if self._tree.stored_kind(path) == 'directory':
 
319
                    parents.append(trans_id)
 
320
            elif self.tree_kind(trans_id) == 'directory':
 
321
                parents.append(trans_id)
 
322
 
 
323
        for parent_id in parents:
 
324
            # ensure that all children are registered with the transaction
 
325
            list(self.iter_tree_children(parent_id))
 
326
 
 
327
    def _has_named_child(self, name, parent_id, known_children):
 
328
        """Does a parent already have a name child.
 
329
 
 
330
        :param name: The searched for name.
 
331
 
 
332
        :param parent_id: The parent for which the check is made.
 
333
 
 
334
        :param known_children: The already known children. This should have
 
335
            been recently obtained from `self.by_parent.get(parent_id)`
 
336
            (or will be if None is passed).
 
337
        """
 
338
        if known_children is None:
 
339
            known_children = self.by_parent().get(parent_id, [])
 
340
        for child in known_children:
 
341
            if self.final_name(child) == name:
 
342
                return True
 
343
        parent_path = self._tree_id_paths.get(parent_id, None)
 
344
        if parent_path is None:
 
345
            # No parent... no children
 
346
            return False
 
347
        child_path = joinpath(parent_path, name)
 
348
        child_id = self._tree_path_ids.get(child_path, None)
 
349
        if child_id is None:
 
350
            # Not known by the tree transform yet, check the filesystem
 
351
            return osutils.lexists(self._tree.abspath(child_path))
 
352
        else:
 
353
            raise AssertionError('child_id is missing: %s, %s, %s'
 
354
                                 % (name, parent_id, child_id))
 
355
 
 
356
    def _available_backup_name(self, name, target_id):
 
357
        """Find an available backup name.
 
358
 
 
359
        :param name: The basename of the file.
 
360
 
 
361
        :param target_id: The directory trans_id where the backup should
 
362
            be placed.
 
363
        """
 
364
        known_children = self.by_parent().get(target_id, [])
 
365
        return osutils.available_backup_name(
 
366
            name,
 
367
            lambda base: self._has_named_child(
 
368
                base, target_id, known_children))
 
369
 
 
370
    def _parent_loops(self):
 
371
        """No entry should be its own ancestor"""
 
372
        conflicts = []
 
373
        for trans_id in self._new_parent:
 
374
            seen = set()
 
375
            parent_id = trans_id
 
376
            while parent_id != ROOT_PARENT:
 
377
                seen.add(parent_id)
 
378
                try:
 
379
                    parent_id = self.final_parent(parent_id)
 
380
                except KeyError:
 
381
                    break
 
382
                if parent_id == trans_id:
 
383
                    conflicts.append(('parent loop', trans_id))
 
384
                if parent_id in seen:
 
385
                    break
 
386
        return conflicts
 
387
 
 
388
    def _improper_versioning(self):
 
389
        """Cannot version a file with no contents, or a bad type.
 
390
 
 
391
        However, existing entries with no contents are okay.
 
392
        """
 
393
        conflicts = []
 
394
        for trans_id in self._new_id:
 
395
            kind = self.final_kind(trans_id)
 
396
            if kind == 'symlink' and not self._tree.supports_symlinks():
 
397
                # Ignore symlinks as they are not supported on this platform
 
398
                continue
 
399
            if kind is None:
 
400
                conflicts.append(('versioning no contents', trans_id))
 
401
                continue
 
402
            if not self._tree.versionable_kind(kind):
 
403
                conflicts.append(('versioning bad kind', trans_id, kind))
 
404
        return conflicts
 
405
 
 
406
    def _executability_conflicts(self):
 
407
        """Check for bad executability changes.
 
408
 
 
409
        Only versioned files may have their executability set, because
 
410
        1. only versioned entries can have executability under windows
 
411
        2. only files can be executable.  (The execute bit on a directory
 
412
           does not indicate searchability)
 
413
        """
 
414
        conflicts = []
 
415
        for trans_id in self._new_executability:
 
416
            if not self.final_is_versioned(trans_id):
 
417
                conflicts.append(('unversioned executability', trans_id))
 
418
            else:
 
419
                if self.final_kind(trans_id) != "file":
 
420
                    conflicts.append(('non-file executability', trans_id))
 
421
        return conflicts
 
422
 
 
423
    def _overwrite_conflicts(self):
 
424
        """Check for overwrites (not permitted on Win32)"""
 
425
        conflicts = []
 
426
        for trans_id in self._new_contents:
 
427
            if self.tree_kind(trans_id) is None:
 
428
                continue
 
429
            if trans_id not in self._removed_contents:
 
430
                conflicts.append(('overwrite', trans_id,
 
431
                                  self.final_name(trans_id)))
 
432
        return conflicts
 
433
 
 
434
    def _duplicate_entries(self, by_parent):
 
435
        """No directory may have two entries with the same name."""
 
436
        conflicts = []
 
437
        if (self._new_name, self._new_parent) == ({}, {}):
 
438
            return conflicts
 
439
        for children in by_parent.values():
 
440
            name_ids = []
 
441
            for child_tid in children:
 
442
                name = self.final_name(child_tid)
 
443
                if name is not None:
 
444
                    # Keep children only if they still exist in the end
 
445
                    if not self._case_sensitive_target:
 
446
                        name = name.lower()
 
447
                    name_ids.append((name, child_tid))
 
448
            name_ids.sort()
 
449
            last_name = None
 
450
            last_trans_id = None
 
451
            for name, trans_id in name_ids:
 
452
                kind = self.final_kind(trans_id)
 
453
                if kind is None and not self.final_is_versioned(trans_id):
 
454
                    continue
 
455
                if name == last_name:
 
456
                    conflicts.append(('duplicate', last_trans_id, trans_id,
 
457
                                      name))
 
458
                last_name = name
 
459
                last_trans_id = trans_id
 
460
        return conflicts
 
461
 
 
462
    def _parent_type_conflicts(self, by_parent):
 
463
        """Children must have a directory parent"""
 
464
        conflicts = []
 
465
        for parent_id, children in by_parent.items():
 
466
            if parent_id == ROOT_PARENT:
 
467
                continue
 
468
            no_children = True
 
469
            for child_id in children:
 
470
                if self.final_kind(child_id) is not None:
 
471
                    no_children = False
 
472
                    break
 
473
            if no_children:
 
474
                continue
 
475
            # There is at least a child, so we need an existing directory to
 
476
            # contain it.
 
477
            kind = self.final_kind(parent_id)
 
478
            if kind is None:
 
479
                # The directory will be deleted
 
480
                conflicts.append(('missing parent', parent_id))
 
481
            elif kind != "directory":
 
482
                # Meh, we need a *directory* to put something in it
 
483
                conflicts.append(('non-directory parent', parent_id))
 
484
        return conflicts
 
485
 
 
486
    def _set_executability(self, path, trans_id):
 
487
        """Set the executability of versioned files """
 
488
        if self._tree._supports_executable():
 
489
            new_executability = self._new_executability[trans_id]
 
490
            abspath = self._tree.abspath(path)
 
491
            current_mode = os.stat(abspath).st_mode
 
492
            if new_executability:
 
493
                umask = os.umask(0)
 
494
                os.umask(umask)
 
495
                to_mode = current_mode | (0o100 & ~umask)
 
496
                # Enable x-bit for others only if they can read it.
 
497
                if current_mode & 0o004:
 
498
                    to_mode |= 0o001 & ~umask
 
499
                if current_mode & 0o040:
 
500
                    to_mode |= 0o010 & ~umask
 
501
            else:
 
502
                to_mode = current_mode & ~0o111
 
503
            osutils.chmod_if_possible(abspath, to_mode)
 
504
 
 
505
    def _new_entry(self, name, parent_id, file_id):
 
506
        """Helper function to create a new filesystem entry."""
 
507
        trans_id = self.create_path(name, parent_id)
 
508
        if file_id is not None:
 
509
            self.version_file(trans_id, file_id=file_id)
 
510
        return trans_id
 
511
 
 
512
    def new_file(self, name, parent_id, contents, file_id=None,
 
513
                 executable=None, sha1=None):
 
514
        """Convenience method to create files.
 
515
 
 
516
        name is the name of the file to create.
 
517
        parent_id is the transaction id of the parent directory of the file.
 
518
        contents is an iterator of bytestrings, which will be used to produce
 
519
        the file.
 
520
        :param file_id: The inventory ID of the file, if it is to be versioned.
 
521
        :param executable: Only valid when a file_id has been supplied.
 
522
        """
 
523
        trans_id = self._new_entry(name, parent_id, file_id)
 
524
        # TODO: rather than scheduling a set_executable call,
 
525
        # have create_file create the file with the right mode.
 
526
        self.create_file(contents, trans_id, sha1=sha1)
 
527
        if executable is not None:
 
528
            self.set_executability(executable, trans_id)
 
529
        return trans_id
 
530
 
 
531
    def new_directory(self, name, parent_id, file_id=None):
 
532
        """Convenience method to create directories.
 
533
 
 
534
        name is the name of the directory to create.
 
535
        parent_id is the transaction id of the parent directory of the
 
536
        directory.
 
537
        file_id is the inventory ID of the directory, if it is to be versioned.
 
538
        """
 
539
        trans_id = self._new_entry(name, parent_id, file_id)
 
540
        self.create_directory(trans_id)
 
541
        return trans_id
 
542
 
 
543
    def new_symlink(self, name, parent_id, target, file_id=None):
 
544
        """Convenience method to create symbolic link.
 
545
 
 
546
        name is the name of the symlink to create.
 
547
        parent_id is the transaction id of the parent directory of the symlink.
 
548
        target is a bytestring of the target of the symlink.
 
549
        file_id is the inventory ID of the file, if it is to be versioned.
 
550
        """
 
551
        trans_id = self._new_entry(name, parent_id, file_id)
 
552
        self.create_symlink(target, trans_id)
 
553
        return trans_id
 
554
 
 
555
    def new_orphan(self, trans_id, parent_id):
 
556
        """Schedule an item to be orphaned.
 
557
 
 
558
        When a directory is about to be removed, its children, if they are not
 
559
        versioned are moved out of the way: they don't have a parent anymore.
 
560
 
 
561
        :param trans_id: The trans_id of the existing item.
 
562
        :param parent_id: The parent trans_id of the item.
 
563
        """
 
564
        raise NotImplementedError(self.new_orphan)
 
565
 
 
566
    def _get_potential_orphans(self, dir_id):
 
567
        """Find the potential orphans in a directory.
 
568
 
 
569
        A directory can't be safely deleted if there are versioned files in it.
 
570
        If all the contained files are unversioned then they can be orphaned.
 
571
 
 
572
        The 'None' return value means that the directory contains at least one
 
573
        versioned file and should not be deleted.
 
574
 
 
575
        :param dir_id: The directory trans id.
 
576
 
 
577
        :return: A list of the orphan trans ids or None if at least one
 
578
             versioned file is present.
 
579
        """
 
580
        orphans = []
 
581
        # Find the potential orphans, stop if one item should be kept
 
582
        for child_tid in self.by_parent()[dir_id]:
 
583
            if child_tid in self._removed_contents:
 
584
                # The child is removed as part of the transform. Since it was
 
585
                # versioned before, it's not an orphan
 
586
                continue
 
587
            if not self.final_is_versioned(child_tid):
 
588
                # The child is not versioned
 
589
                orphans.append(child_tid)
 
590
            else:
 
591
                # We have a versioned file here, searching for orphans is
 
592
                # meaningless.
 
593
                orphans = None
 
594
                break
 
595
        return orphans
 
596
 
 
597
    def _affected_ids(self):
 
598
        """Return the set of transform ids affected by the transform"""
 
599
        trans_ids = set(self._removed_id)
 
600
        trans_ids.update(self._new_id)
 
601
        trans_ids.update(self._removed_contents)
 
602
        trans_ids.update(self._new_contents)
 
603
        trans_ids.update(self._new_executability)
 
604
        trans_ids.update(self._new_name)
 
605
        trans_ids.update(self._new_parent)
 
606
        return trans_ids
 
607
 
 
608
    def _get_file_id_maps(self):
 
609
        """Return mapping of file_ids to trans_ids in the to and from states"""
 
610
        trans_ids = self._affected_ids()
 
611
        from_trans_ids = {}
 
612
        to_trans_ids = {}
 
613
        # Build up two dicts: trans_ids associated with file ids in the
 
614
        # FROM state, vs the TO state.
 
615
        for trans_id in trans_ids:
 
616
            from_file_id = self.tree_file_id(trans_id)
 
617
            if from_file_id is not None:
 
618
                from_trans_ids[from_file_id] = trans_id
 
619
            to_file_id = self.final_file_id(trans_id)
 
620
            if to_file_id is not None:
 
621
                to_trans_ids[to_file_id] = trans_id
 
622
        return from_trans_ids, to_trans_ids
 
623
 
 
624
    def _from_file_data(self, from_trans_id, from_versioned, from_path):
 
625
        """Get data about a file in the from (tree) state
 
626
 
 
627
        Return a (name, parent, kind, executable) tuple
 
628
        """
 
629
        from_path = self._tree_id_paths.get(from_trans_id)
 
630
        if from_versioned:
 
631
            # get data from working tree if versioned
 
632
            from_entry = next(self._tree.iter_entries_by_dir(
 
633
                specific_files=[from_path]))[1]
 
634
            from_name = from_entry.name
 
635
            from_parent = from_entry.parent_id
 
636
        else:
 
637
            from_entry = None
 
638
            if from_path is None:
 
639
                # File does not exist in FROM state
 
640
                from_name = None
 
641
                from_parent = None
 
642
            else:
 
643
                # File exists, but is not versioned.  Have to use path-
 
644
                # splitting stuff
 
645
                from_name = os.path.basename(from_path)
 
646
                tree_parent = self.get_tree_parent(from_trans_id)
 
647
                from_parent = self.tree_file_id(tree_parent)
 
648
        if from_path is not None:
 
649
            from_kind, from_executable, from_stats = \
 
650
                self._tree._comparison_data(from_entry, from_path)
 
651
        else:
 
652
            from_kind = None
 
653
            from_executable = False
 
654
        return from_name, from_parent, from_kind, from_executable
 
655
 
 
656
    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
 
657
        """Get data about a file in the to (target) state
 
658
 
 
659
        Return a (name, parent, kind, executable) tuple
 
660
        """
 
661
        to_name = self.final_name(to_trans_id)
 
662
        to_kind = self.final_kind(to_trans_id)
 
663
        to_parent = self.final_file_id(self.final_parent(to_trans_id))
 
664
        if to_trans_id in self._new_executability:
 
665
            to_executable = self._new_executability[to_trans_id]
 
666
        elif to_trans_id == from_trans_id:
 
667
            to_executable = from_executable
 
668
        else:
 
669
            to_executable = False
 
670
        return to_name, to_parent, to_kind, to_executable
 
671
 
 
672
    def iter_changes(self):
 
673
        """Produce output in the same format as Tree.iter_changes.
 
674
 
 
675
        Will produce nonsensical results if invoked while inventory/filesystem
 
676
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
 
677
 
 
678
        This reads the Transform, but only reproduces changes involving a
 
679
        file_id.  Files that are not versioned in either of the FROM or TO
 
680
        states are not reflected.
 
681
        """
 
682
        final_paths = FinalPaths(self)
 
683
        from_trans_ids, to_trans_ids = self._get_file_id_maps()
 
684
        results = []
 
685
        # Now iterate through all active file_ids
 
686
        for file_id in set(from_trans_ids).union(to_trans_ids):
 
687
            modified = False
 
688
            from_trans_id = from_trans_ids.get(file_id)
 
689
            # find file ids, and determine versioning state
 
690
            if from_trans_id is None:
 
691
                from_versioned = False
 
692
                from_trans_id = to_trans_ids[file_id]
 
693
            else:
 
694
                from_versioned = True
 
695
            to_trans_id = to_trans_ids.get(file_id)
 
696
            if to_trans_id is None:
 
697
                to_versioned = False
 
698
                to_trans_id = from_trans_id
 
699
            else:
 
700
                to_versioned = True
 
701
 
 
702
            if not from_versioned:
 
703
                from_path = None
 
704
            else:
 
705
                from_path = self._tree_id_paths.get(from_trans_id)
 
706
            if not to_versioned:
 
707
                to_path = None
 
708
            else:
 
709
                to_path = final_paths.get_path(to_trans_id)
 
710
 
 
711
            from_name, from_parent, from_kind, from_executable = \
 
712
                self._from_file_data(from_trans_id, from_versioned, from_path)
 
713
 
 
714
            to_name, to_parent, to_kind, to_executable = \
 
715
                self._to_file_data(to_trans_id, from_trans_id, from_executable)
 
716
 
 
717
            if from_kind != to_kind:
 
718
                modified = True
 
719
            elif to_kind in ('file', 'symlink') and (
 
720
                    to_trans_id != from_trans_id
 
721
                    or to_trans_id in self._new_contents):
 
722
                modified = True
 
723
            if (not modified and from_versioned == to_versioned
 
724
                and from_parent == to_parent and from_name == to_name
 
725
                    and from_executable == to_executable):
 
726
                continue
 
727
            results.append(
 
728
                InventoryTreeChange(
 
729
                    file_id, (from_path, to_path), modified,
 
730
                    (from_versioned, to_versioned),
 
731
                    (from_parent, to_parent),
 
732
                    (from_name, to_name),
 
733
                    (from_kind, to_kind),
 
734
                    (from_executable, to_executable)))
 
735
 
 
736
        def path_key(c):
 
737
            return (c.path[0] or '', c.path[1] or '')
 
738
        return iter(sorted(results, key=path_key))
 
739
 
 
740
    def get_preview_tree(self):
 
741
        """Return a tree representing the result of the transform.
 
742
 
 
743
        The tree is a snapshot, and altering the TreeTransform will invalidate
 
744
        it.
 
745
        """
 
746
        raise NotImplementedError(self.get_preview_tree)
 
747
 
 
748
    def commit(self, branch, message, merge_parents=None, strict=False,
 
749
               timestamp=None, timezone=None, committer=None, authors=None,
 
750
               revprops=None, revision_id=None):
 
751
        """Commit the result of this TreeTransform to a branch.
 
752
 
 
753
        :param branch: The branch to commit to.
 
754
        :param message: The message to attach to the commit.
 
755
        :param merge_parents: Additional parent revision-ids specified by
 
756
            pending merges.
 
757
        :param strict: If True, abort the commit if there are unversioned
 
758
            files.
 
759
        :param timestamp: if not None, seconds-since-epoch for the time and
 
760
            date.  (May be a float.)
 
761
        :param timezone: Optional timezone for timestamp, as an offset in
 
762
            seconds.
 
763
        :param committer: Optional committer in email-id format.
 
764
            (e.g. "J Random Hacker <jrandom@example.com>")
 
765
        :param authors: Optional list of authors in email-id format.
 
766
        :param revprops: Optional dictionary of revision properties.
 
767
        :param revision_id: Optional revision id.  (Specifying a revision-id
 
768
            may reduce performance for some non-native formats.)
 
769
        :return: The revision_id of the revision committed.
 
770
        """
 
771
        self._check_malformed()
 
772
        if strict:
 
773
            unversioned = set(self._new_contents).difference(set(self._new_id))
 
774
            for trans_id in unversioned:
 
775
                if not self.final_is_versioned(trans_id):
 
776
                    raise errors.StrictCommitFailed()
 
777
 
 
778
        revno, last_rev_id = branch.last_revision_info()
 
779
        if last_rev_id == _mod_revision.NULL_REVISION:
 
780
            if merge_parents is not None:
 
781
                raise ValueError('Cannot supply merge parents for first'
 
782
                                 ' commit.')
 
783
            parent_ids = []
 
784
        else:
 
785
            parent_ids = [last_rev_id]
 
786
            if merge_parents is not None:
 
787
                parent_ids.extend(merge_parents)
 
788
        if self._tree.get_revision_id() != last_rev_id:
 
789
            raise ValueError('TreeTransform not based on branch basis: %s' %
 
790
                             self._tree.get_revision_id().decode('utf-8'))
 
791
        from .. import commit
 
792
        revprops = commit.Commit.update_revprops(revprops, branch, authors)
 
793
        builder = branch.get_commit_builder(parent_ids,
 
794
                                            timestamp=timestamp,
 
795
                                            timezone=timezone,
 
796
                                            committer=committer,
 
797
                                            revprops=revprops,
 
798
                                            revision_id=revision_id)
 
799
        preview = self.get_preview_tree()
 
800
        list(builder.record_iter_changes(preview, last_rev_id,
 
801
                                         self.iter_changes()))
 
802
        builder.finish_inventory()
 
803
        revision_id = builder.commit(message)
 
804
        branch.set_last_revision_info(revno + 1, revision_id)
 
805
        return revision_id
 
806
 
 
807
    def _text_parent(self, trans_id):
 
808
        path = self.tree_path(trans_id)
 
809
        try:
 
810
            if path is None or self._tree.kind(path) != 'file':
 
811
                return None
 
812
        except errors.NoSuchFile:
 
813
            return None
 
814
        return path
 
815
 
 
816
    def _get_parents_texts(self, trans_id):
 
817
        """Get texts for compression parents of this file."""
 
818
        path = self._text_parent(trans_id)
 
819
        if path is None:
 
820
            return ()
 
821
        return (self._tree.get_file_text(path),)
 
822
 
 
823
    def _get_parents_lines(self, trans_id):
 
824
        """Get lines for compression parents of this file."""
 
825
        path = self._text_parent(trans_id)
 
826
        if path is None:
 
827
            return ()
 
828
        return (self._tree.get_file_lines(path),)
 
829
 
 
830
    def serialize(self, serializer):
 
831
        """Serialize this TreeTransform.
 
832
 
 
833
        :param serializer: A Serialiser like pack.ContainerSerializer.
 
834
        """
 
835
        from .. import bencode
 
836
        new_name = {k.encode('utf-8'): v.encode('utf-8')
 
837
                    for k, v in self._new_name.items()}
 
838
        new_parent = {k.encode('utf-8'): v.encode('utf-8')
 
839
                      for k, v in self._new_parent.items()}
 
840
        new_id = {k.encode('utf-8'): v
 
841
                  for k, v in self._new_id.items()}
 
842
        new_executability = {k.encode('utf-8'): int(v)
 
843
                             for k, v in self._new_executability.items()}
 
844
        tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
 
845
                         for k, v in self._tree_path_ids.items()}
 
846
        non_present_ids = {k: v.encode('utf-8')
 
847
                           for k, v in self._non_present_ids.items()}
 
848
        removed_contents = [trans_id.encode('utf-8')
 
849
                            for trans_id in self._removed_contents]
 
850
        removed_id = [trans_id.encode('utf-8')
 
851
                      for trans_id in self._removed_id]
 
852
        attribs = {
 
853
            b'_id_number': self._id_number,
 
854
            b'_new_name': new_name,
 
855
            b'_new_parent': new_parent,
 
856
            b'_new_executability': new_executability,
 
857
            b'_new_id': new_id,
 
858
            b'_tree_path_ids': tree_path_ids,
 
859
            b'_removed_id': removed_id,
 
860
            b'_removed_contents': removed_contents,
 
861
            b'_non_present_ids': non_present_ids,
 
862
            }
 
863
        yield serializer.bytes_record(bencode.bencode(attribs),
 
864
                                      ((b'attribs',),))
 
865
        for trans_id, kind in sorted(self._new_contents.items()):
 
866
            if kind == 'file':
 
867
                with open(self._limbo_name(trans_id), 'rb') as cur_file:
 
868
                    lines = cur_file.readlines()
 
869
                parents = self._get_parents_lines(trans_id)
 
870
                mpdiff = multiparent.MultiParent.from_lines(lines, parents)
 
871
                content = b''.join(mpdiff.to_patch())
 
872
            if kind == 'directory':
 
873
                content = b''
 
874
            if kind == 'symlink':
 
875
                content = self._read_symlink_target(trans_id)
 
876
                if not isinstance(content, bytes):
 
877
                    content = content.encode('utf-8')
 
878
            yield serializer.bytes_record(
 
879
                content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
 
880
 
 
881
    def deserialize(self, records):
 
882
        """Deserialize a stored TreeTransform.
 
883
 
 
884
        :param records: An iterable of (names, content) tuples, as per
 
885
            pack.ContainerPushParser.
 
886
        """
 
887
        from .. import bencode
 
888
        names, content = next(records)
 
889
        attribs = bencode.bdecode(content)
 
890
        self._id_number = attribs[b'_id_number']
 
891
        self._new_name = {k.decode('utf-8'): v.decode('utf-8')
 
892
                          for k, v in attribs[b'_new_name'].items()}
 
893
        self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
 
894
                            for k, v in attribs[b'_new_parent'].items()}
 
895
        self._new_executability = {
 
896
            k.decode('utf-8'): bool(v)
 
897
            for k, v in attribs[b'_new_executability'].items()}
 
898
        self._new_id = {k.decode('utf-8'): v
 
899
                        for k, v in attribs[b'_new_id'].items()}
 
900
        self._r_new_id = {v: k for k, v in self._new_id.items()}
 
901
        self._tree_path_ids = {}
 
902
        self._tree_id_paths = {}
 
903
        for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
 
904
            path = bytepath.decode('utf-8')
 
905
            trans_id = trans_id.decode('utf-8')
 
906
            self._tree_path_ids[path] = trans_id
 
907
            self._tree_id_paths[trans_id] = path
 
908
        self._removed_id = {trans_id.decode('utf-8')
 
909
                            for trans_id in attribs[b'_removed_id']}
 
910
        self._removed_contents = set(
 
911
            trans_id.decode('utf-8')
 
912
            for trans_id in attribs[b'_removed_contents'])
 
913
        self._non_present_ids = {
 
914
            k: v.decode('utf-8')
 
915
            for k, v in attribs[b'_non_present_ids'].items()}
 
916
        for ((trans_id, kind),), content in records:
 
917
            trans_id = trans_id.decode('utf-8')
 
918
            kind = kind.decode('ascii')
 
919
            if kind == 'file':
 
920
                mpdiff = multiparent.MultiParent.from_patch(content)
 
921
                lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
 
922
                self.create_file(lines, trans_id)
 
923
            if kind == 'directory':
 
924
                self.create_directory(trans_id)
 
925
            if kind == 'symlink':
 
926
                self.create_symlink(content.decode('utf-8'), trans_id)
 
927
 
 
928
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
 
929
        """Schedule creation of a new file.
 
930
 
 
931
        :seealso: new_file.
 
932
 
 
933
        :param contents: an iterator of strings, all of which will be written
 
934
            to the target destination.
 
935
        :param trans_id: TreeTransform handle
 
936
        :param mode_id: If not None, force the mode of the target file to match
 
937
            the mode of the object referenced by mode_id.
 
938
            Otherwise, we will try to preserve mode bits of an existing file.
 
939
        :param sha1: If the sha1 of this content is already known, pass it in.
 
940
            We can use it to prevent future sha1 computations.
 
941
        """
 
942
        raise NotImplementedError(self.create_file)
 
943
 
 
944
    def create_directory(self, trans_id):
 
945
        """Schedule creation of a new directory.
 
946
 
 
947
        See also new_directory.
 
948
        """
 
949
        raise NotImplementedError(self.create_directory)
 
950
 
 
951
    def create_symlink(self, target, trans_id):
 
952
        """Schedule creation of a new symbolic link.
 
953
 
 
954
        target is a bytestring.
 
955
        See also new_symlink.
 
956
        """
 
957
        raise NotImplementedError(self.create_symlink)
 
958
 
 
959
    def create_hardlink(self, path, trans_id):
 
960
        """Schedule creation of a hard link"""
 
961
        raise NotImplementedError(self.create_hardlink)
 
962
 
 
963
    def cancel_creation(self, trans_id):
 
964
        """Cancel the creation of new file contents."""
 
965
        raise NotImplementedError(self.cancel_creation)
 
966
 
 
967
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
 
968
        """Apply all changes to the inventory and filesystem.
 
969
 
 
970
        If filesystem or inventory conflicts are present, MalformedTransform
 
971
        will be thrown.
 
972
 
 
973
        If apply succeeds, finalize is not necessary.
 
974
 
 
975
        :param no_conflicts: if True, the caller guarantees there are no
 
976
            conflicts, so no check is made.
 
977
        :param precomputed_delta: An inventory delta to use instead of
 
978
            calculating one.
 
979
        :param _mover: Supply an alternate FileMover, for testing
 
980
        """
 
981
        raise NotImplementedError(self.apply)
 
982
 
 
983
    def cook_conflicts(self, raw_conflicts):
 
984
        """Generate a list of cooked conflicts, sorted by file path"""
 
985
        if not raw_conflicts:
 
986
            return
 
987
        fp = FinalPaths(self)
 
988
        from .workingtree import TextConflict
 
989
        for c in raw_conflicts:
 
990
            if c[0] == 'text conflict':
 
991
                yield TextConflict(fp.get_path(c[1]))
 
992
            elif c[0] == 'duplicate':
 
993
                yield TextConflict(fp.get_path(c[2]))
 
994
            elif c[0] == 'contents conflict':
 
995
                yield TextConflict(fp.get_path(c[1][0]))
 
996
            elif c[0] == 'missing parent':
 
997
                # TODO(jelmer): This should not make it to here
 
998
                yield TextConflict(fp.get_path(c[2]))
 
999
            else:
 
1000
                raise AssertionError('unknown conflict %s' % c[0])
 
1001
 
 
1002
 
 
1003
class DiskTreeTransform(TreeTransformBase):
 
1004
    """Tree transform storing its contents on disk."""
 
1005
 
 
1006
    def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
 
1007
        """Constructor.
 
1008
        :param tree: The tree that will be transformed, but not necessarily
 
1009
            the output tree.
 
1010
        :param limbodir: A directory where new files can be stored until
 
1011
            they are installed in their proper places
 
1012
        :param pb: ignored
 
1013
        :param case_sensitive: If True, the target of the transform is
 
1014
            case sensitive, not just case preserving.
 
1015
        """
 
1016
        TreeTransformBase.__init__(self, tree, pb, case_sensitive)
 
1017
        self._limbodir = limbodir
 
1018
        self._deletiondir = None
 
1019
        # A mapping of transform ids to their limbo filename
 
1020
        self._limbo_files = {}
 
1021
        self._possibly_stale_limbo_files = set()
 
1022
        # A mapping of transform ids to a set of the transform ids of children
 
1023
        # that their limbo directory has
 
1024
        self._limbo_children = {}
 
1025
        # Map transform ids to maps of child filename to child transform id
 
1026
        self._limbo_children_names = {}
 
1027
        # List of transform ids that need to be renamed from limbo into place
 
1028
        self._needs_rename = set()
 
1029
        self._creation_mtime = None
 
1030
        self._create_symlinks = osutils.supports_symlinks(self._limbodir)
 
1031
 
 
1032
    def finalize(self):
 
1033
        """Release the working tree lock, if held, clean up limbo dir.
 
1034
 
 
1035
        This is required if apply has not been invoked, but can be invoked
 
1036
        even after apply.
 
1037
        """
 
1038
        if self._tree is None:
 
1039
            return
 
1040
        try:
 
1041
            limbo_paths = list(self._limbo_files.values())
 
1042
            limbo_paths.extend(self._possibly_stale_limbo_files)
 
1043
            limbo_paths.sort(reverse=True)
 
1044
            for path in limbo_paths:
 
1045
                try:
 
1046
                    osutils.delete_any(path)
 
1047
                except OSError as e:
 
1048
                    if e.errno != errno.ENOENT:
 
1049
                        raise
 
1050
                    # XXX: warn? perhaps we just got interrupted at an
 
1051
                    # inconvenient moment, but perhaps files are disappearing
 
1052
                    # from under us?
 
1053
            try:
 
1054
                osutils.delete_any(self._limbodir)
 
1055
            except OSError:
 
1056
                # We don't especially care *why* the dir is immortal.
 
1057
                raise ImmortalLimbo(self._limbodir)
 
1058
            try:
 
1059
                if self._deletiondir is not None:
 
1060
                    osutils.delete_any(self._deletiondir)
 
1061
            except OSError:
 
1062
                raise errors.ImmortalPendingDeletion(self._deletiondir)
 
1063
        finally:
 
1064
            TreeTransformBase.finalize(self)
 
1065
 
 
1066
    def _limbo_supports_executable(self):
 
1067
        """Check if the limbo path supports the executable bit."""
 
1068
        return osutils.supports_executable(self._limbodir)
 
1069
 
 
1070
    def _limbo_name(self, trans_id):
 
1071
        """Generate the limbo name of a file"""
 
1072
        limbo_name = self._limbo_files.get(trans_id)
 
1073
        if limbo_name is None:
 
1074
            limbo_name = self._generate_limbo_path(trans_id)
 
1075
            self._limbo_files[trans_id] = limbo_name
 
1076
        return limbo_name
 
1077
 
 
1078
    def _generate_limbo_path(self, trans_id):
 
1079
        """Generate a limbo path using the trans_id as the relative path.
 
1080
 
 
1081
        This is suitable as a fallback, and when the transform should not be
 
1082
        sensitive to the path encoding of the limbo directory.
 
1083
        """
 
1084
        self._needs_rename.add(trans_id)
 
1085
        return osutils.pathjoin(self._limbodir, trans_id)
 
1086
 
 
1087
    def adjust_path(self, name, parent, trans_id):
 
1088
        previous_parent = self._new_parent.get(trans_id)
 
1089
        previous_name = self._new_name.get(trans_id)
 
1090
        super(DiskTreeTransform, self).adjust_path(name, parent, trans_id)
 
1091
        if (trans_id in self._limbo_files
 
1092
                and trans_id not in self._needs_rename):
 
1093
            self._rename_in_limbo([trans_id])
 
1094
            if previous_parent != parent:
 
1095
                self._limbo_children[previous_parent].remove(trans_id)
 
1096
            if previous_parent != parent or previous_name != name:
 
1097
                del self._limbo_children_names[previous_parent][previous_name]
 
1098
 
 
1099
    def _rename_in_limbo(self, trans_ids):
 
1100
        """Fix limbo names so that the right final path is produced.
 
1101
 
 
1102
        This means we outsmarted ourselves-- we tried to avoid renaming
 
1103
        these files later by creating them with their final names in their
 
1104
        final parents.  But now the previous name or parent is no longer
 
1105
        suitable, so we have to rename them.
 
1106
 
 
1107
        Even for trans_ids that have no new contents, we must remove their
 
1108
        entries from _limbo_files, because they are now stale.
 
1109
        """
 
1110
        for trans_id in trans_ids:
 
1111
            old_path = self._limbo_files[trans_id]
 
1112
            self._possibly_stale_limbo_files.add(old_path)
 
1113
            del self._limbo_files[trans_id]
 
1114
            if trans_id not in self._new_contents:
 
1115
                continue
 
1116
            new_path = self._limbo_name(trans_id)
 
1117
            os.rename(old_path, new_path)
 
1118
            self._possibly_stale_limbo_files.remove(old_path)
 
1119
            for descendant in self._limbo_descendants(trans_id):
 
1120
                desc_path = self._limbo_files[descendant]
 
1121
                desc_path = new_path + desc_path[len(old_path):]
 
1122
                self._limbo_files[descendant] = desc_path
 
1123
 
 
1124
    def _limbo_descendants(self, trans_id):
 
1125
        """Return the set of trans_ids whose limbo paths descend from this."""
 
1126
        descendants = set(self._limbo_children.get(trans_id, []))
 
1127
        for descendant in list(descendants):
 
1128
            descendants.update(self._limbo_descendants(descendant))
 
1129
        return descendants
 
1130
 
 
1131
    def _set_mode(self, trans_id, mode_id, typefunc):
 
1132
        raise NotImplementedError(self._set_mode)
 
1133
 
 
1134
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
 
1135
        """Schedule creation of a new file.
 
1136
 
 
1137
        :seealso: new_file.
 
1138
 
 
1139
        :param contents: an iterator of strings, all of which will be written
 
1140
            to the target destination.
 
1141
        :param trans_id: TreeTransform handle
 
1142
        :param mode_id: If not None, force the mode of the target file to match
 
1143
            the mode of the object referenced by mode_id.
 
1144
            Otherwise, we will try to preserve mode bits of an existing file.
 
1145
        :param sha1: If the sha1 of this content is already known, pass it in.
 
1146
            We can use it to prevent future sha1 computations.
 
1147
        """
 
1148
        name = self._limbo_name(trans_id)
 
1149
        with open(name, 'wb') as f:
 
1150
            unique_add(self._new_contents, trans_id, 'file')
 
1151
            f.writelines(contents)
 
1152
        self._set_mtime(name)
 
1153
        self._set_mode(trans_id, mode_id, S_ISREG)
 
1154
        # It is unfortunate we have to use lstat instead of fstat, but we just
 
1155
        # used utime and chmod on the file, so we need the accurate final
 
1156
        # details.
 
1157
        if sha1 is not None:
 
1158
            self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name))
 
1159
 
 
1160
    def _read_symlink_target(self, trans_id):
 
1161
        return os.readlink(self._limbo_name(trans_id))
 
1162
 
 
1163
    def _set_mtime(self, path):
 
1164
        """All files that are created get the same mtime.
 
1165
 
 
1166
        This time is set by the first object to be created.
 
1167
        """
 
1168
        if self._creation_mtime is None:
 
1169
            self._creation_mtime = time.time()
 
1170
        os.utime(path, (self._creation_mtime, self._creation_mtime))
 
1171
 
 
1172
    def create_hardlink(self, path, trans_id):
 
1173
        """Schedule creation of a hard link"""
 
1174
        name = self._limbo_name(trans_id)
 
1175
        try:
 
1176
            os.link(path, name)
 
1177
        except OSError as e:
 
1178
            if e.errno != errno.EPERM:
 
1179
                raise
 
1180
            raise errors.HardLinkNotSupported(path)
 
1181
        try:
 
1182
            unique_add(self._new_contents, trans_id, 'file')
 
1183
        except BaseException:
 
1184
            # Clean up the file, it never got registered so
 
1185
            # TreeTransform.finalize() won't clean it up.
 
1186
            os.unlink(name)
 
1187
            raise
 
1188
 
 
1189
    def create_directory(self, trans_id):
 
1190
        """Schedule creation of a new directory.
 
1191
 
 
1192
        See also new_directory.
 
1193
        """
 
1194
        os.mkdir(self._limbo_name(trans_id))
 
1195
        unique_add(self._new_contents, trans_id, 'directory')
 
1196
 
 
1197
    def create_symlink(self, target, trans_id):
 
1198
        """Schedule creation of a new symbolic link.
 
1199
 
 
1200
        target is a bytestring.
 
1201
        See also new_symlink.
 
1202
        """
 
1203
        if self._create_symlinks:
 
1204
            os.symlink(target, self._limbo_name(trans_id))
 
1205
        else:
 
1206
            try:
 
1207
                path = FinalPaths(self).get_path(trans_id)
 
1208
            except KeyError:
 
1209
                path = None
 
1210
            trace.warning(
 
1211
                'Unable to create symlink "%s" on this filesystem.' % (path,))
 
1212
        # We add symlink to _new_contents even if they are unsupported
 
1213
        # and not created. These entries are subsequently used to avoid
 
1214
        # conflicts on platforms that don't support symlink
 
1215
        unique_add(self._new_contents, trans_id, 'symlink')
 
1216
 
 
1217
    def cancel_creation(self, trans_id):
 
1218
        """Cancel the creation of new file contents."""
 
1219
        del self._new_contents[trans_id]
 
1220
        if trans_id in self._observed_sha1s:
 
1221
            del self._observed_sha1s[trans_id]
 
1222
        children = self._limbo_children.get(trans_id)
 
1223
        # if this is a limbo directory with children, move them before removing
 
1224
        # the directory
 
1225
        if children is not None:
 
1226
            self._rename_in_limbo(children)
 
1227
            del self._limbo_children[trans_id]
 
1228
            del self._limbo_children_names[trans_id]
 
1229
        osutils.delete_any(self._limbo_name(trans_id))
 
1230
 
 
1231
    def new_orphan(self, trans_id, parent_id):
 
1232
        conf = self._tree.get_config_stack()
 
1233
        handle_orphan = conf.get('transform.orphan_policy')
 
1234
        handle_orphan(self, trans_id, parent_id)
 
1235
 
 
1236
 
 
1237
class GitTreeTransform(DiskTreeTransform):
 
1238
    """Represent a tree transformation.
 
1239
 
 
1240
    This object is designed to support incremental generation of the transform,
 
1241
    in any order.
 
1242
 
 
1243
    However, it gives optimum performance when parent directories are created
 
1244
    before their contents.  The transform is then able to put child files
 
1245
    directly in their parent directory, avoiding later renames.
 
1246
 
 
1247
    It is easy to produce malformed transforms, but they are generally
 
1248
    harmless.  Attempting to apply a malformed transform will cause an
 
1249
    exception to be raised before any modifications are made to the tree.
 
1250
 
 
1251
    Many kinds of malformed transforms can be corrected with the
 
1252
    resolve_conflicts function.  The remaining ones indicate programming error,
 
1253
    such as trying to create a file with no path.
 
1254
 
 
1255
    Two sets of file creation methods are supplied.  Convenience methods are:
 
1256
     * new_file
 
1257
     * new_directory
 
1258
     * new_symlink
 
1259
 
 
1260
    These are composed of the low-level methods:
 
1261
     * create_path
 
1262
     * create_file or create_directory or create_symlink
 
1263
     * version_file
 
1264
     * set_executability
 
1265
 
 
1266
    Transform/Transaction ids
 
1267
    -------------------------
 
1268
    trans_ids are temporary ids assigned to all files involved in a transform.
 
1269
    It's possible, even common, that not all files in the Tree have trans_ids.
 
1270
 
 
1271
    trans_ids are used because filenames and file_ids are not good enough
 
1272
    identifiers; filenames change.
 
1273
 
 
1274
    trans_ids are only valid for the TreeTransform that generated them.
 
1275
 
 
1276
    Limbo
 
1277
    -----
 
1278
    Limbo is a temporary directory use to hold new versions of files.
 
1279
    Files are added to limbo by create_file, create_directory, create_symlink,
 
1280
    and their convenience variants (new_*).  Files may be removed from limbo
 
1281
    using cancel_creation.  Files are renamed from limbo into their final
 
1282
    location as part of TreeTransform.apply
 
1283
 
 
1284
    Limbo must be cleaned up, by either calling TreeTransform.apply or
 
1285
    calling TreeTransform.finalize.
 
1286
 
 
1287
    Files are placed into limbo inside their parent directories, where
 
1288
    possible.  This reduces subsequent renames, and makes operations involving
 
1289
    lots of files faster.  This optimization is only possible if the parent
 
1290
    directory is created *before* creating any of its children, so avoid
 
1291
    creating children before parents, where possible.
 
1292
 
 
1293
    Pending-deletion
 
1294
    ----------------
 
1295
    This temporary directory is used by _FileMover for storing files that are
 
1296
    about to be deleted.  In case of rollback, the files will be restored.
 
1297
    FileMover does not delete files until it is sure that a rollback will not
 
1298
    happen.
 
1299
    """
 
1300
 
 
1301
    def __init__(self, tree, pb=None):
 
1302
        """Note: a tree_write lock is taken on the tree.
 
1303
 
 
1304
        Use TreeTransform.finalize() to release the lock (can be omitted if
 
1305
        TreeTransform.apply() called).
 
1306
        """
 
1307
        tree.lock_tree_write()
 
1308
        try:
 
1309
            limbodir = urlutils.local_path_from_url(
 
1310
                tree._transport.abspath('limbo'))
 
1311
            osutils.ensure_empty_directory_exists(
 
1312
                limbodir,
 
1313
                errors.ExistingLimbo)
 
1314
            deletiondir = urlutils.local_path_from_url(
 
1315
                tree._transport.abspath('pending-deletion'))
 
1316
            osutils.ensure_empty_directory_exists(
 
1317
                deletiondir,
 
1318
                errors.ExistingPendingDeletion)
 
1319
        except BaseException:
 
1320
            tree.unlock()
 
1321
            raise
 
1322
 
 
1323
        # Cache of realpath results, to speed up canonical_path
 
1324
        self._realpaths = {}
 
1325
        # Cache of relpath results, to speed up canonical_path
 
1326
        self._relpaths = {}
 
1327
        DiskTreeTransform.__init__(self, tree, limbodir, pb,
 
1328
                                   tree.case_sensitive)
 
1329
        self._deletiondir = deletiondir
 
1330
 
 
1331
    def canonical_path(self, path):
 
1332
        """Get the canonical tree-relative path"""
 
1333
        # don't follow final symlinks
 
1334
        abs = self._tree.abspath(path)
 
1335
        if abs in self._relpaths:
 
1336
            return self._relpaths[abs]
 
1337
        dirname, basename = os.path.split(abs)
 
1338
        if dirname not in self._realpaths:
 
1339
            self._realpaths[dirname] = os.path.realpath(dirname)
 
1340
        dirname = self._realpaths[dirname]
 
1341
        abs = osutils.pathjoin(dirname, basename)
 
1342
        if dirname in self._relpaths:
 
1343
            relpath = osutils.pathjoin(self._relpaths[dirname], basename)
 
1344
            relpath = relpath.rstrip('/\\')
 
1345
        else:
 
1346
            relpath = self._tree.relpath(abs)
 
1347
        self._relpaths[abs] = relpath
 
1348
        return relpath
 
1349
 
 
1350
    def tree_kind(self, trans_id):
 
1351
        """Determine the file kind in the working tree.
 
1352
 
 
1353
        :returns: The file kind or None if the file does not exist
 
1354
        """
 
1355
        path = self._tree_id_paths.get(trans_id)
 
1356
        if path is None:
 
1357
            return None
 
1358
        try:
 
1359
            return osutils.file_kind(self._tree.abspath(path))
 
1360
        except errors.NoSuchFile:
 
1361
            return None
 
1362
 
 
1363
    def _set_mode(self, trans_id, mode_id, typefunc):
 
1364
        """Set the mode of new file contents.
 
1365
        The mode_id is the existing file to get the mode from (often the same
 
1366
        as trans_id).  The operation is only performed if there's a mode match
 
1367
        according to typefunc.
 
1368
        """
 
1369
        if mode_id is None:
 
1370
            mode_id = trans_id
 
1371
        try:
 
1372
            old_path = self._tree_id_paths[mode_id]
 
1373
        except KeyError:
 
1374
            return
 
1375
        try:
 
1376
            mode = os.stat(self._tree.abspath(old_path)).st_mode
 
1377
        except OSError as e:
 
1378
            if e.errno in (errno.ENOENT, errno.ENOTDIR):
 
1379
                # Either old_path doesn't exist, or the parent of the
 
1380
                # target is not a directory (but will be one eventually)
 
1381
                # Either way, we know it doesn't exist *right now*
 
1382
                # See also bug #248448
 
1383
                return
 
1384
            else:
 
1385
                raise
 
1386
        if typefunc(mode):
 
1387
            osutils.chmod_if_possible(self._limbo_name(trans_id), mode)
 
1388
 
 
1389
    def iter_tree_children(self, parent_id):
 
1390
        """Iterate through the entry's tree children, if any"""
 
1391
        try:
 
1392
            path = self._tree_id_paths[parent_id]
 
1393
        except KeyError:
 
1394
            return
 
1395
        try:
 
1396
            children = os.listdir(self._tree.abspath(path))
 
1397
        except OSError as e:
 
1398
            if not (osutils._is_error_enotdir(e) or
 
1399
                    e.errno in (errno.ENOENT, errno.ESRCH)):
 
1400
                raise
 
1401
            return
 
1402
 
 
1403
        for child in children:
 
1404
            childpath = joinpath(path, child)
 
1405
            if self._tree.is_control_filename(childpath):
 
1406
                continue
 
1407
            yield self.trans_id_tree_path(childpath)
 
1408
 
 
1409
    def _generate_limbo_path(self, trans_id):
 
1410
        """Generate a limbo path using the final path if possible.
 
1411
 
 
1412
        This optimizes the performance of applying the tree transform by
 
1413
        avoiding renames.  These renames can be avoided only when the parent
 
1414
        directory is already scheduled for creation.
 
1415
 
 
1416
        If the final path cannot be used, falls back to using the trans_id as
 
1417
        the relpath.
 
1418
        """
 
1419
        parent = self._new_parent.get(trans_id)
 
1420
        # if the parent directory is already in limbo (e.g. when building a
 
1421
        # tree), choose a limbo name inside the parent, to reduce further
 
1422
        # renames.
 
1423
        use_direct_path = False
 
1424
        if self._new_contents.get(parent) == 'directory':
 
1425
            filename = self._new_name.get(trans_id)
 
1426
            if filename is not None:
 
1427
                if parent not in self._limbo_children:
 
1428
                    self._limbo_children[parent] = set()
 
1429
                    self._limbo_children_names[parent] = {}
 
1430
                    use_direct_path = True
 
1431
                # the direct path can only be used if no other file has
 
1432
                # already taken this pathname, i.e. if the name is unused, or
 
1433
                # if it is already associated with this trans_id.
 
1434
                elif self._case_sensitive_target:
 
1435
                    if (self._limbo_children_names[parent].get(filename)
 
1436
                            in (trans_id, None)):
 
1437
                        use_direct_path = True
 
1438
                else:
 
1439
                    for l_filename, l_trans_id in (
 
1440
                            self._limbo_children_names[parent].items()):
 
1441
                        if l_trans_id == trans_id:
 
1442
                            continue
 
1443
                        if l_filename.lower() == filename.lower():
 
1444
                            break
 
1445
                    else:
 
1446
                        use_direct_path = True
 
1447
 
 
1448
        if not use_direct_path:
 
1449
            return DiskTreeTransform._generate_limbo_path(self, trans_id)
 
1450
 
 
1451
        limbo_name = osutils.pathjoin(self._limbo_files[parent], filename)
 
1452
        self._limbo_children[parent].add(trans_id)
 
1453
        self._limbo_children_names[parent][filename] = trans_id
 
1454
        return limbo_name
 
1455
 
 
1456
    def version_file(self, trans_id, file_id=None):
 
1457
        """Schedule a file to become versioned."""
 
1458
        if file_id is None:
 
1459
            raise ValueError()
 
1460
        unique_add(self._new_id, trans_id, file_id)
 
1461
        unique_add(self._r_new_id, file_id, trans_id)
 
1462
 
 
1463
    def cancel_versioning(self, trans_id):
 
1464
        """Undo a previous versioning of a file"""
 
1465
        file_id = self._new_id[trans_id]
 
1466
        del self._new_id[trans_id]
 
1467
        del self._r_new_id[file_id]
 
1468
 
 
1469
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
 
1470
        """Apply all changes to the inventory and filesystem.
 
1471
 
 
1472
        If filesystem or inventory conflicts are present, MalformedTransform
 
1473
        will be thrown.
 
1474
 
 
1475
        If apply succeeds, finalize is not necessary.
 
1476
 
 
1477
        :param no_conflicts: if True, the caller guarantees there are no
 
1478
            conflicts, so no check is made.
 
1479
        :param precomputed_delta: An inventory delta to use instead of
 
1480
            calculating one.
 
1481
        :param _mover: Supply an alternate FileMover, for testing
 
1482
        """
 
1483
        for hook in MutableTree.hooks['pre_transform']:
 
1484
            hook(self._tree, self)
 
1485
        if not no_conflicts:
 
1486
            self._check_malformed()
 
1487
        self.rename_count = 0
 
1488
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
1489
            if precomputed_delta is None:
 
1490
                child_pb.update(gettext('Apply phase'), 0, 2)
 
1491
                changes = self._generate_transform_changes()
 
1492
                offset = 1
 
1493
            else:
 
1494
                changes = [
 
1495
                    (op, np, ie) for (op, np, fid, ie) in precomputed_delta]
 
1496
                offset = 0
 
1497
            if _mover is None:
 
1498
                mover = _FileMover()
 
1499
            else:
 
1500
                mover = _mover
 
1501
            try:
 
1502
                child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset)
 
1503
                self._apply_removals(mover)
 
1504
                child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset)
 
1505
                modified_paths = self._apply_insertions(mover)
 
1506
            except BaseException:
 
1507
                mover.rollback()
 
1508
                raise
 
1509
            else:
 
1510
                mover.apply_deletions()
 
1511
        if self.final_file_id(self.root) is None:
 
1512
            changes = [e for e in changes if e[0] != '']
 
1513
        self._tree._apply_transform_delta(changes)
 
1514
        self._done = True
 
1515
        self.finalize()
 
1516
        return _TransformResults(modified_paths, self.rename_count)
 
1517
 
 
1518
    def _apply_removals(self, mover):
 
1519
        """Perform tree operations that remove directory/inventory names.
 
1520
 
 
1521
        That is, delete files that are to be deleted, and put any files that
 
1522
        need renaming into limbo.  This must be done in strict child-to-parent
 
1523
        order.
 
1524
 
 
1525
        If inventory_delta is None, no inventory delta generation is performed.
 
1526
        """
 
1527
        tree_paths = sorted(self._tree_path_ids.items(), reverse=True)
 
1528
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
1529
            for num, (path, trans_id) in enumerate(tree_paths):
 
1530
                # do not attempt to move root into a subdirectory of itself.
 
1531
                if path == '':
 
1532
                    continue
 
1533
                child_pb.update(gettext('removing file'), num, len(tree_paths))
 
1534
                full_path = self._tree.abspath(path)
 
1535
                if trans_id in self._removed_contents:
 
1536
                    delete_path = os.path.join(self._deletiondir, trans_id)
 
1537
                    mover.pre_delete(full_path, delete_path)
 
1538
                elif (trans_id in self._new_name or
 
1539
                      trans_id in self._new_parent):
 
1540
                    try:
 
1541
                        mover.rename(full_path, self._limbo_name(trans_id))
 
1542
                    except TransformRenameFailed as e:
 
1543
                        if e.errno != errno.ENOENT:
 
1544
                            raise
 
1545
                    else:
 
1546
                        self.rename_count += 1
 
1547
 
 
1548
    def _apply_insertions(self, mover):
 
1549
        """Perform tree operations that insert directory/inventory names.
 
1550
 
 
1551
        That is, create any files that need to be created, and restore from
 
1552
        limbo any files that needed renaming.  This must be done in strict
 
1553
        parent-to-child order.
 
1554
 
 
1555
        If inventory_delta is None, no inventory delta is calculated, and
 
1556
        no list of modified paths is returned.
 
1557
        """
 
1558
        new_paths = self.new_paths(filesystem_only=True)
 
1559
        modified_paths = []
 
1560
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
1561
            for num, (path, trans_id) in enumerate(new_paths):
 
1562
                if (num % 10) == 0:
 
1563
                    child_pb.update(gettext('adding file'),
 
1564
                                    num, len(new_paths))
 
1565
                full_path = self._tree.abspath(path)
 
1566
                if trans_id in self._needs_rename:
 
1567
                    try:
 
1568
                        mover.rename(self._limbo_name(trans_id), full_path)
 
1569
                    except TransformRenameFailed as e:
 
1570
                        # We may be renaming a dangling inventory id
 
1571
                        if e.errno != errno.ENOENT:
 
1572
                            raise
 
1573
                    else:
 
1574
                        self.rename_count += 1
 
1575
                    # TODO: if trans_id in self._observed_sha1s, we should
 
1576
                    #       re-stat the final target, since ctime will be
 
1577
                    #       updated by the change.
 
1578
                if (trans_id in self._new_contents
 
1579
                        or self.path_changed(trans_id)):
 
1580
                    if trans_id in self._new_contents:
 
1581
                        modified_paths.append(full_path)
 
1582
                if trans_id in self._new_executability:
 
1583
                    self._set_executability(path, trans_id)
 
1584
                if trans_id in self._observed_sha1s:
 
1585
                    o_sha1, o_st_val = self._observed_sha1s[trans_id]
 
1586
                    st = osutils.lstat(full_path)
 
1587
                    self._observed_sha1s[trans_id] = (o_sha1, st)
 
1588
        for path, trans_id in new_paths:
 
1589
            # new_paths includes stuff like workingtree conflicts. Only the
 
1590
            # stuff in new_contents actually comes from limbo.
 
1591
            if trans_id in self._limbo_files:
 
1592
                del self._limbo_files[trans_id]
 
1593
        self._new_contents.clear()
 
1594
        return modified_paths
 
1595
 
 
1596
    def _inventory_altered(self):
 
1597
        """Determine which trans_ids need new Inventory entries.
 
1598
 
 
1599
        An new entry is needed when anything that would be reflected by an
 
1600
        inventory entry changes, including file name, file_id, parent file_id,
 
1601
        file kind, and the execute bit.
 
1602
 
 
1603
        Some care is taken to return entries with real changes, not cases
 
1604
        where the value is deleted and then restored to its original value,
 
1605
        but some actually unchanged values may be returned.
 
1606
 
 
1607
        :returns: A list of (path, trans_id) for all items requiring an
 
1608
            inventory change. Ordered by path.
 
1609
        """
 
1610
        changed_ids = set()
 
1611
        # Find entries whose file_ids are new (or changed).
 
1612
        new_file_id = set(t for t in self._new_id
 
1613
                          if self._new_id[t] != self.tree_file_id(t))
 
1614
        for id_set in [self._new_name, self._new_parent, new_file_id,
 
1615
                       self._new_executability]:
 
1616
            changed_ids.update(id_set)
 
1617
        # removing implies a kind change
 
1618
        changed_kind = set(self._removed_contents)
 
1619
        # so does adding
 
1620
        changed_kind.intersection_update(self._new_contents)
 
1621
        # Ignore entries that are already known to have changed.
 
1622
        changed_kind.difference_update(changed_ids)
 
1623
        #  to keep only the truly changed ones
 
1624
        changed_kind = (t for t in changed_kind
 
1625
                        if self.tree_kind(t) != self.final_kind(t))
 
1626
        # all kind changes will alter the inventory
 
1627
        changed_ids.update(changed_kind)
 
1628
        # To find entries with changed parent_ids, find parents which existed,
 
1629
        # but changed file_id.
 
1630
        # Now add all their children to the set.
 
1631
        for parent_trans_id in new_file_id:
 
1632
            changed_ids.update(self.iter_tree_children(parent_trans_id))
 
1633
        return sorted(FinalPaths(self).get_paths(changed_ids))
 
1634
 
 
1635
    def _generate_transform_changes(self):
 
1636
        """Generate an inventory delta for the current transform."""
 
1637
        changes = []
 
1638
        new_paths = self._inventory_altered()
 
1639
        total_entries = len(new_paths) + len(self._removed_id)
 
1640
        with ui.ui_factory.nested_progress_bar() as child_pb:
 
1641
            for num, trans_id in enumerate(self._removed_id):
 
1642
                if (num % 10) == 0:
 
1643
                    child_pb.update(gettext('removing file'),
 
1644
                                    num, total_entries)
 
1645
                if trans_id == self._new_root:
 
1646
                    file_id = self._tree.path2id('')
 
1647
                else:
 
1648
                    file_id = self.tree_file_id(trans_id)
 
1649
                # File-id isn't really being deleted, just moved
 
1650
                if file_id in self._r_new_id:
 
1651
                    continue
 
1652
                path = self._tree_id_paths[trans_id]
 
1653
                changes.append((path, None, None))
 
1654
            new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
 
1655
                                     new_paths)
 
1656
            for num, (path, trans_id) in enumerate(new_paths):
 
1657
                if (num % 10) == 0:
 
1658
                    child_pb.update(gettext('adding file'),
 
1659
                                    num + len(self._removed_id), total_entries)
 
1660
                file_id = new_path_file_ids[trans_id]
 
1661
                if file_id is None:
 
1662
                    continue
 
1663
                kind = self.final_kind(trans_id)
 
1664
                if kind is None:
 
1665
                    kind = self._tree.stored_kind(self._tree.id2path(file_id))
 
1666
                parent_trans_id = self.final_parent(trans_id)
 
1667
                parent_file_id = new_path_file_ids.get(parent_trans_id)
 
1668
                if parent_file_id is None:
 
1669
                    parent_file_id = self.final_file_id(parent_trans_id)
 
1670
                if trans_id in self._new_reference_revision:
 
1671
                    new_entry = inventory.TreeReference(
 
1672
                        file_id,
 
1673
                        self._new_name[trans_id],
 
1674
                        self.final_file_id(self._new_parent[trans_id]),
 
1675
                        None, self._new_reference_revision[trans_id])
 
1676
                else:
 
1677
                    new_entry = inventory.make_entry(kind,
 
1678
                                                     self.final_name(trans_id),
 
1679
                                                     parent_file_id, file_id)
 
1680
                try:
 
1681
                    old_path = self._tree.id2path(new_entry.file_id)
 
1682
                except errors.NoSuchId:
 
1683
                    old_path = None
 
1684
                new_executability = self._new_executability.get(trans_id)
 
1685
                if new_executability is not None:
 
1686
                    new_entry.executable = new_executability
 
1687
                changes.append(
 
1688
                    (old_path, path, new_entry))
 
1689
        return changes