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