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