/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
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
7490.77.12 by Jelmer Vernooij
Merge lp:~jelmer/brz/transform.
20
import errno
21
import os
7490.84.1 by Jelmer Vernooij
Split out git and bzr-specific transform.
22
from stat import S_ISREG
23
import time
7490.77.12 by Jelmer Vernooij
Merge lp:~jelmer/brz/transform.
24
7490.84.1 by Jelmer Vernooij
Split out git and bzr-specific transform.
25
from .. import errors, multiparent, osutils, trace, ui, urlutils
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
26
from ..i18n import gettext
27
from ..mutabletree import MutableTree
7490.84.1 by Jelmer Vernooij
Split out git and bzr-specific transform.
28
from ..sixish import viewitems, viewvalues
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
29
from ..transform import (
7490.84.1 by Jelmer Vernooij
Split out git and bzr-specific transform.
30
    TreeTransform,
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
31
    _TransformResults,
32
    _FileMover,
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
33
    FinalPaths,
7490.77.17 by Jelmer Vernooij
Rationalize TreeTransform class hierarchy.
34
    joinpath,
7490.77.9 by Jelmer Vernooij
Move out some file id handling functions.
35
    unique_add,
7490.77.12 by Jelmer Vernooij
Merge lp:~jelmer/brz/transform.
36
    TransformRenameFailed,
7490.84.1 by Jelmer Vernooij
Split out git and bzr-specific transform.
37
    ImmortalLimbo,
38
    ROOT_PARENT,
39
    ReusingTransform,
40
    MalformedTransform,
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
41
    )
42
43
from ..bzr import inventory
7490.77.12 by Jelmer Vernooij
Merge lp:~jelmer/brz/transform.
44
from ..bzr.transform import TransformPreview as GitTransformPreview
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
45
46
7490.84.1 by Jelmer Vernooij
Split out git and bzr-specific transform.
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
7490.77.17 by Jelmer Vernooij
Rationalize TreeTransform class hierarchy.
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
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
1451
7490.77.9 by Jelmer Vernooij
Move out some file id handling functions.
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
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
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()
7490.77.16 by Jelmer Vernooij
Move more generic code.
1483
        self.rename_count = 0
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
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)
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
1487
                changes = self._generate_transform_changes()
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
1488
                offset = 1
1489
            else:
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
1490
                changes = [
1491
                    (op, np, ie) for (op, np, fid, ie) in precomputed_delta]
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
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:
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
1508
            changes = [e for e in changes if e[0] != '']
1509
        self._tree._apply_transform_delta(changes)
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
1510
        self._done = True
1511
        self.finalize()
1512
        return _TransformResults(modified_paths, self.rename_count)
1513
7490.77.12 by Jelmer Vernooij
Merge lp:~jelmer/brz/transform.
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
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
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):
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
1632
        """Generate an inventory delta for the current transform."""
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
1633
        changes = []
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
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]
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
1649
                changes.append((path, None, None))
7490.77.2 by Jelmer Vernooij
Split out git and bzr-specific transforms.
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
7490.77.3 by Jelmer Vernooij
Move inventory_altered.
1683
                changes.append(
1684
                    (old_path, path, new_entry))
1685
        return changes