/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-05-24 00:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 7504.
  • Revision ID: jelmer@jelmer.uk-20200524003950-bbc545r76vc5yajg
Add github action.

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