/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: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-07-20 02:17:05 UTC
  • mfrom: (7518.1.2 merge-3.1)
  • Revision ID: breezy.the.bot@gmail.com-20200720021705-5f11tmo1hdqjxm6x
Merge lp:brz/3.1.

Merged from https://code.launchpad.net/~jelmer/brz/merge-3.1/+merge/387628

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