/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-07-18 23:14:00 UTC
  • mfrom: (7490.40.62 work)
  • mto: This revision was merged to the branch mainline in revision 7519.
  • Revision ID: jelmer@jelmer.uk-20200718231400-jaes9qltn8oi8xss
Merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

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