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