/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/git/transform.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-08-23 01:15:41 UTC
  • mfrom: (7520.1.4 merge-3.1)
  • Revision ID: breezy.the.bot@gmail.com-20200823011541-nv0oh7nzaganx2qy
Merge lp:brz/3.1.

Merged from https://code.launchpad.net/~jelmer/brz/merge-3.1/+merge/389690

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
 
20
20
import errno
21
21
import os
22
 
from stat import S_ISREG
 
22
import posixpath
 
23
from stat import S_IEXEC, S_ISREG
23
24
import time
24
25
 
25
 
from .. import errors, multiparent, osutils, trace, ui, urlutils
 
26
from .mapping import encode_git_path, mode_kind, mode_is_executable, object_mode
 
27
from .tree import GitTree, GitTreeDirectory, GitTreeSymlink, GitTreeFile
 
28
 
 
29
from .. import (
 
30
    annotate,
 
31
    conflicts,
 
32
    errors,
 
33
    multiparent,
 
34
    osutils,
 
35
    revision as _mod_revision,
 
36
    trace,
 
37
    ui,
 
38
    urlutils,
 
39
    )
26
40
from ..i18n import gettext
27
41
from ..mutabletree import MutableTree
 
42
from ..tree import InterTree, TreeChange
28
43
from ..transform import (
 
44
    PreviewTree,
29
45
    TreeTransform,
30
46
    _TransformResults,
31
47
    _FileMover,
39
55
    MalformedTransform,
40
56
    )
41
57
 
42
 
from ..bzr import inventory
43
 
from ..bzr.transform import TransformPreview as GitTransformPreview
 
58
from dulwich.index import commit_tree, blob_from_path_and_stat
 
59
from dulwich.objects import Blob
44
60
 
45
61
 
46
62
class TreeTransformBase(TreeTransform):
58
74
        super(TreeTransformBase, self).__init__(tree, pb=pb)
59
75
        # mapping of trans_id => (sha1 of content, stat_value)
60
76
        self._observed_sha1s = {}
61
 
        # Mapping of trans_id -> new file_id
62
 
        self._new_id = {}
63
 
        # Mapping of old file-id -> trans_id
64
 
        self._non_present_ids = {}
65
 
        # Mapping of new file_id -> trans_id
66
 
        self._r_new_id = {}
 
77
        # Set of versioned trans ids
 
78
        self._versioned = set()
67
79
        # The trans_id that will be used as the tree root
68
 
        if tree.is_versioned(''):
69
 
            self._new_root = self.trans_id_tree_path('')
70
 
        else:
71
 
            self._new_root = None
 
80
        self.root = self.trans_id_tree_path('')
72
81
        # Whether the target is case sensitive
73
82
        self._case_sensitive_target = case_sensitive
 
83
        self._symlink_target = {}
 
84
 
 
85
    @property
 
86
    def mapping(self):
 
87
        return self._tree.mapping
74
88
 
75
89
    def finalize(self):
76
90
        """Release the working tree lock, if held.
85
99
        self._tree.unlock()
86
100
        self._tree = None
87
101
 
88
 
    def __get_root(self):
89
 
        return self._new_root
90
 
 
91
 
    root = property(__get_root)
92
 
 
93
102
    def create_path(self, name, parent):
94
103
        """Assign a transaction id to a new path"""
95
 
        trans_id = self._assign_id()
 
104
        trans_id = self.assign_id()
96
105
        unique_add(self._new_name, trans_id, name)
97
106
        unique_add(self._new_parent, trans_id, parent)
98
107
        return trans_id
99
108
 
100
109
    def adjust_root_path(self, name, parent):
101
110
        """Emulate moving the root by moving all children, instead.
102
 
 
103
 
        We do this by undoing the association of root's transaction id with the
104
 
        current tree.  This allows us to create a new directory with that
105
 
        transaction id.  We unversion the root directory and version the
106
 
        physically new directory, and hope someone versions the tree root
107
 
        later.
108
111
        """
109
 
        old_root = self._new_root
110
 
        old_root_file_id = self.final_file_id(old_root)
111
 
        # force moving all children of root
112
 
        for child_id in self.iter_tree_children(old_root):
113
 
            if child_id != parent:
114
 
                self.adjust_path(self.final_name(child_id),
115
 
                                 self.final_parent(child_id), child_id)
116
 
            file_id = self.final_file_id(child_id)
117
 
            if file_id is not None:
118
 
                self.unversion_file(child_id)
119
 
            self.version_file(child_id, file_id=file_id)
120
 
 
121
 
        # the physical root needs a new transaction id
122
 
        self._tree_path_ids.pop("")
123
 
        self._tree_id_paths.pop(old_root)
124
 
        self._new_root = self.trans_id_tree_path('')
125
 
        if parent == old_root:
126
 
            parent = self._new_root
127
 
        self.adjust_path(name, parent, old_root)
128
 
        self.create_directory(old_root)
129
 
        self.version_file(old_root, file_id=old_root_file_id)
130
 
        self.unversion_file(self._new_root)
131
112
 
132
113
    def fixup_new_roots(self):
133
114
        """Reinterpret requests to change the root directory
147
128
            return
148
129
        if len(new_roots) != 1:
149
130
            raise ValueError('A tree cannot have two roots!')
150
 
        if self._new_root is None:
151
 
            self._new_root = new_roots[0]
152
 
            return
153
131
        old_new_root = new_roots[0]
154
132
        # unversion the new root's directory.
155
 
        if self.final_kind(self._new_root) is None:
156
 
            file_id = self.final_file_id(old_new_root)
157
 
        else:
158
 
            file_id = self.final_file_id(self._new_root)
159
 
        if old_new_root in self._new_id:
 
133
        if old_new_root in self._versioned:
160
134
            self.cancel_versioning(old_new_root)
161
135
        else:
162
136
            self.unversion_file(old_new_root)
163
 
        # if, at this stage, root still has an old file_id, zap it so we can
164
 
        # stick a new one in.
165
 
        if (self.tree_file_id(self._new_root) is not None
166
 
                and self._new_root not in self._removed_id):
167
 
            self.unversion_file(self._new_root)
168
 
        if file_id is not None:
169
 
            self.version_file(self._new_root, file_id=file_id)
170
137
 
171
138
        # Now move children of new root into old root directory.
172
139
        # Ensure all children are registered with the transaction, but don't
174
141
        list(self.iter_tree_children(old_new_root))
175
142
        # Move all children of new root into old root directory.
176
143
        for child in self.by_parent().get(old_new_root, []):
177
 
            self.adjust_path(self.final_name(child), self._new_root, child)
 
144
            self.adjust_path(self.final_name(child), self.root, child)
178
145
 
179
146
        # Ensure old_new_root has no directory.
180
147
        if old_new_root in self._new_contents:
183
150
            self.delete_contents(old_new_root)
184
151
 
185
152
        # prevent deletion of root directory.
186
 
        if self._new_root in self._removed_contents:
187
 
            self.cancel_deletion(self._new_root)
 
153
        if self.root in self._removed_contents:
 
154
            self.cancel_deletion(self.root)
188
155
 
189
156
        # destroy path info for old_new_root.
190
157
        del self._new_parent[old_new_root]
198
165
        """
199
166
        if file_id is None:
200
167
            raise ValueError('None is not a valid file id')
201
 
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
202
 
            return self._r_new_id[file_id]
203
 
        else:
204
 
            try:
205
 
                path = self._tree.id2path(file_id)
206
 
            except errors.NoSuchId:
207
 
                if file_id in self._non_present_ids:
208
 
                    return self._non_present_ids[file_id]
209
 
                else:
210
 
                    trans_id = self._assign_id()
211
 
                    self._non_present_ids[file_id] = trans_id
212
 
                    return trans_id
213
 
            else:
214
 
                return self.trans_id_tree_path(path)
 
168
        path = self.mapping.parse_file_id(file_id)
 
169
        return self.trans_id_tree_path(path)
215
170
 
216
171
    def version_file(self, trans_id, file_id=None):
217
172
        """Schedule a file to become versioned."""
218
 
        raise NotImplementedError(self.version_file)
 
173
        if trans_id in self._versioned:
 
174
            raise errors.DuplicateKey(key=trans_id)
 
175
        self._versioned.add(trans_id)
219
176
 
220
177
    def cancel_versioning(self, trans_id):
221
178
        """Undo a previous versioning of a file"""
232
189
            stale_ids = self._needs_rename.difference(self._new_name)
233
190
            stale_ids.difference_update(self._new_parent)
234
191
            stale_ids.difference_update(self._new_contents)
235
 
            stale_ids.difference_update(self._new_id)
 
192
            stale_ids.difference_update(self._versioned)
236
193
            needs_rename = self._needs_rename.difference(stale_ids)
237
194
            id_sets = (needs_rename, self._new_executability)
238
195
        else:
239
196
            id_sets = (self._new_name, self._new_parent, self._new_contents,
240
 
                       self._new_id, self._new_executability)
 
197
                       self._versioned, self._new_executability)
241
198
        for id_set in id_sets:
242
199
            new_ids.update(id_set)
243
200
        return sorted(FinalPaths(self).get_paths(new_ids))
244
201
 
245
 
    def tree_file_id(self, trans_id):
246
 
        """Determine the file id associated with the trans_id in the tree"""
247
 
        path = self.tree_path(trans_id)
248
 
        if path is None:
249
 
            return None
250
 
        # the file is old; the old id is still valid
251
 
        if self._new_root == trans_id:
252
 
            return self._tree.path2id('')
253
 
        return self._tree.path2id(path)
254
 
 
255
202
    def final_is_versioned(self, trans_id):
256
 
        return self.final_file_id(trans_id) is not None
257
 
 
258
 
    def final_file_id(self, trans_id):
259
 
        """Determine the file id after any changes are applied, or None.
260
 
 
261
 
        None indicates that the file will not be versioned after changes are
262
 
        applied.
263
 
        """
264
 
        try:
265
 
            return self._new_id[trans_id]
266
 
        except KeyError:
267
 
            if trans_id in self._removed_id:
268
 
                return None
269
 
        return self.tree_file_id(trans_id)
270
 
 
271
 
    def inactive_file_id(self, trans_id):
272
 
        """Return the inactive file_id associated with a transaction id.
273
 
        That is, the one in the tree or in non_present_ids.
274
 
        The file_id may actually be active, too.
275
 
        """
276
 
        file_id = self.tree_file_id(trans_id)
277
 
        if file_id is not None:
278
 
            return file_id
279
 
        for key, value in self._non_present_ids.items():
280
 
            if value == trans_id:
281
 
                return key
282
 
 
283
 
    def find_conflicts(self):
 
203
        if trans_id in self._versioned:
 
204
            return True
 
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)
 
211
 
 
212
    def find_raw_conflicts(self):
284
213
        """Find any violations of inventory or filesystem invariants"""
285
214
        if self._done is True:
286
215
            raise ReusingTransform()
289
218
        # all children of non-existent parents are known, by definition.
290
219
        self._add_tree_children()
291
220
        by_parent = self.by_parent()
292
 
        conflicts.extend(self._unversioned_parents(by_parent))
293
221
        conflicts.extend(self._parent_loops())
294
222
        conflicts.extend(self._duplicate_entries(by_parent))
295
223
        conflicts.extend(self._parent_type_conflicts(by_parent))
299
227
        return conflicts
300
228
 
301
229
    def _check_malformed(self):
302
 
        conflicts = self.find_conflicts()
 
230
        conflicts = self.find_raw_conflicts()
303
231
        if len(conflicts) != 0:
304
232
            raise MalformedTransform(conflicts=conflicts)
305
233
 
315
243
        for trans_id in self._removed_id:
316
244
            path = self.tree_path(trans_id)
317
245
            if path is not None:
318
 
                if self._tree.stored_kind(path) == 'directory':
319
 
                    parents.append(trans_id)
 
246
                try:
 
247
                    if self._tree.stored_kind(path) == 'directory':
 
248
                        parents.append(trans_id)
 
249
                except errors.NoSuchFile:
 
250
                    pass
320
251
            elif self.tree_kind(trans_id) == 'directory':
321
252
                parents.append(trans_id)
322
253
 
385
316
                    break
386
317
        return conflicts
387
318
 
388
 
    def _unversioned_parents(self, by_parent):
389
 
        """If parent directories are versioned, children must be versioned."""
390
 
        conflicts = []
391
 
        for parent_id, children in by_parent.items():
392
 
            if parent_id == ROOT_PARENT:
393
 
                continue
394
 
            if self.final_is_versioned(parent_id):
395
 
                continue
396
 
            for child_id in children:
397
 
                if self.final_is_versioned(child_id):
398
 
                    conflicts.append(('unversioned parent', parent_id))
399
 
                    break
400
 
        return conflicts
401
 
 
402
319
    def _improper_versioning(self):
403
320
        """Cannot version a file with no contents, or a bad type.
404
321
 
405
322
        However, existing entries with no contents are okay.
406
323
        """
407
324
        conflicts = []
408
 
        for trans_id in self._new_id:
 
325
        for trans_id in self._versioned:
409
326
            kind = self.final_kind(trans_id)
410
327
            if kind == 'symlink' and not self._tree.supports_symlinks():
411
328
                # Ignore symlinks as they are not supported on this platform
611
528
    def _affected_ids(self):
612
529
        """Return the set of transform ids affected by the transform"""
613
530
        trans_ids = set(self._removed_id)
614
 
        trans_ids.update(self._new_id)
 
531
        trans_ids.update(self._versioned)
615
532
        trans_ids.update(self._removed_contents)
616
533
        trans_ids.update(self._new_contents)
617
534
        trans_ids.update(self._new_executability)
619
536
        trans_ids.update(self._new_parent)
620
537
        return trans_ids
621
538
 
622
 
    def _get_file_id_maps(self):
623
 
        """Return mapping of file_ids to trans_ids in the to and from states"""
624
 
        trans_ids = self._affected_ids()
625
 
        from_trans_ids = {}
626
 
        to_trans_ids = {}
627
 
        # Build up two dicts: trans_ids associated with file ids in the
628
 
        # FROM state, vs the TO state.
629
 
        for trans_id in trans_ids:
630
 
            from_file_id = self.tree_file_id(trans_id)
631
 
            if from_file_id is not None:
632
 
                from_trans_ids[from_file_id] = trans_id
633
 
            to_file_id = self.final_file_id(trans_id)
634
 
            if to_file_id is not None:
635
 
                to_trans_ids[to_file_id] = trans_id
636
 
        return from_trans_ids, to_trans_ids
637
 
 
638
 
    def _from_file_data(self, from_trans_id, from_versioned, from_path):
639
 
        """Get data about a file in the from (tree) state
640
 
 
641
 
        Return a (name, parent, kind, executable) tuple
642
 
        """
643
 
        from_path = self._tree_id_paths.get(from_trans_id)
644
 
        if from_versioned:
645
 
            # get data from working tree if versioned
646
 
            from_entry = next(self._tree.iter_entries_by_dir(
647
 
                specific_files=[from_path]))[1]
648
 
            from_name = from_entry.name
649
 
            from_parent = from_entry.parent_id
650
 
        else:
651
 
            from_entry = None
652
 
            if from_path is None:
653
 
                # File does not exist in FROM state
654
 
                from_name = None
655
 
                from_parent = None
656
 
            else:
657
 
                # File exists, but is not versioned.  Have to use path-
658
 
                # splitting stuff
659
 
                from_name = os.path.basename(from_path)
660
 
                tree_parent = self.get_tree_parent(from_trans_id)
661
 
                from_parent = self.tree_file_id(tree_parent)
662
 
        if from_path is not None:
663
 
            from_kind, from_executable, from_stats = \
664
 
                self._tree._comparison_data(from_entry, from_path)
665
 
        else:
666
 
            from_kind = None
667
 
            from_executable = False
668
 
        return from_name, from_parent, from_kind, from_executable
669
 
 
670
 
    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
671
 
        """Get data about a file in the to (target) state
672
 
 
673
 
        Return a (name, parent, kind, executable) tuple
674
 
        """
675
 
        to_name = self.final_name(to_trans_id)
676
 
        to_kind = self.final_kind(to_trans_id)
677
 
        to_parent = self.final_file_id(self.final_parent(to_trans_id))
678
 
        if to_trans_id in self._new_executability:
679
 
            to_executable = self._new_executability[to_trans_id]
680
 
        elif to_trans_id == from_trans_id:
681
 
            to_executable = from_executable
682
 
        else:
683
 
            to_executable = False
684
 
        return to_name, to_parent, to_kind, to_executable
685
 
 
686
 
    def iter_changes(self):
 
539
    def iter_changes(self, want_unversioned=False):
687
540
        """Produce output in the same format as Tree.iter_changes.
688
541
 
689
542
        Will produce nonsensical results if invoked while inventory/filesystem
690
 
        conflicts (as reported by TreeTransform.find_conflicts()) are present.
691
 
 
692
 
        This reads the Transform, but only reproduces changes involving a
693
 
        file_id.  Files that are not versioned in either of the FROM or TO
694
 
        states are not reflected.
 
543
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
695
544
        """
696
545
        final_paths = FinalPaths(self)
697
 
        from_trans_ids, to_trans_ids = self._get_file_id_maps()
 
546
        trans_ids = self._affected_ids()
698
547
        results = []
699
 
        # Now iterate through all active file_ids
700
 
        for file_id in set(from_trans_ids).union(to_trans_ids):
 
548
        # Now iterate through all active paths
 
549
        for trans_id in trans_ids:
 
550
            from_path = self.tree_path(trans_id)
701
551
            modified = False
702
 
            from_trans_id = from_trans_ids.get(file_id)
703
552
            # find file ids, and determine versioning state
704
 
            if from_trans_id is None:
 
553
            if from_path is None:
705
554
                from_versioned = False
706
 
                from_trans_id = to_trans_ids[file_id]
707
555
            else:
708
 
                from_versioned = True
709
 
            to_trans_id = to_trans_ids.get(file_id)
710
 
            if to_trans_id is None:
 
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:
711
561
                to_versioned = False
712
 
                to_trans_id = from_trans_id
713
 
            else:
714
 
                to_versioned = True
715
 
 
716
 
            if not from_versioned:
717
 
                from_path = None
718
 
            else:
719
 
                from_path = self._tree_id_paths.get(from_trans_id)
720
 
            if not to_versioned:
 
562
            else:
 
563
                to_versioned = self.final_is_versioned(trans_id)
 
564
            if not want_unversioned and not to_versioned:
721
565
                to_path = None
722
 
            else:
723
 
                to_path = final_paths.get_path(to_trans_id)
724
 
 
725
 
            from_name, from_parent, from_kind, from_executable = \
726
 
                self._from_file_data(from_trans_id, from_versioned, from_path)
727
 
 
728
 
            to_name, to_parent, to_kind, to_executable = \
729
 
                self._to_file_data(to_trans_id, from_trans_id, from_executable)
730
 
 
731
 
            if from_kind != to_kind:
 
566
 
 
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
 
587
 
 
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:
 
593
                to_executable = from_executable
 
594
 
 
595
            if from_versioned and from_kind != to_kind:
732
596
                modified = True
733
597
            elif to_kind in ('file', 'symlink') and (
734
 
                    to_trans_id != from_trans_id
735
 
                    or to_trans_id in self._new_contents):
 
598
                    trans_id in self._new_contents):
736
599
                modified = True
737
600
            if (not modified and from_versioned == to_versioned
738
 
                and from_parent == to_parent and from_name == to_name
 
601
                and from_path == to_path
 
602
                and from_name == to_name
739
603
                    and from_executable == to_executable):
740
604
                continue
 
605
            if (from_path, to_path) == (None, None):
 
606
                continue
741
607
            results.append(
742
608
                TreeChange(
743
 
                    file_id, (from_path, to_path), modified,
 
609
                    (from_path, to_path), modified,
744
610
                    (from_versioned, to_versioned),
745
 
                    (from_parent, to_parent),
746
611
                    (from_name, to_name),
747
612
                    (from_kind, to_kind),
748
613
                    (from_executable, to_executable)))
757
622
        The tree is a snapshot, and altering the TreeTransform will invalidate
758
623
        it.
759
624
        """
760
 
        raise NotImplementedError(self.get_preview_tree)
 
625
        return GitPreviewTree(self)
761
626
 
762
627
    def commit(self, branch, message, merge_parents=None, strict=False,
763
628
               timestamp=None, timezone=None, committer=None, authors=None,
784
649
        """
785
650
        self._check_malformed()
786
651
        if strict:
787
 
            unversioned = set(self._new_contents).difference(set(self._new_id))
 
652
            unversioned = set(self._new_contents).difference(set(self._versioned))
788
653
            for trans_id in unversioned:
789
654
                if not self.final_is_versioned(trans_id):
790
655
                    raise errors.StrictCommitFailed()
841
706
            return ()
842
707
        return (self._tree.get_file_lines(path),)
843
708
 
844
 
    def serialize(self, serializer):
845
 
        """Serialize this TreeTransform.
846
 
 
847
 
        :param serializer: A Serialiser like pack.ContainerSerializer.
848
 
        """
849
 
        from .. import bencode
850
 
        new_name = {k.encode('utf-8'): v.encode('utf-8')
851
 
                    for k, v in self._new_name.items()}
852
 
        new_parent = {k.encode('utf-8'): v.encode('utf-8')
853
 
                      for k, v in self._new_parent.items()}
854
 
        new_id = {k.encode('utf-8'): v
855
 
                  for k, v in self._new_id.items()}
856
 
        new_executability = {k.encode('utf-8'): int(v)
857
 
                             for k, v in self._new_executability.items()}
858
 
        tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
859
 
                         for k, v in self._tree_path_ids.items()}
860
 
        non_present_ids = {k: v.encode('utf-8')
861
 
                           for k, v in self._non_present_ids.items()}
862
 
        removed_contents = [trans_id.encode('utf-8')
863
 
                            for trans_id in self._removed_contents]
864
 
        removed_id = [trans_id.encode('utf-8')
865
 
                      for trans_id in self._removed_id]
866
 
        attribs = {
867
 
            b'_id_number': self._id_number,
868
 
            b'_new_name': new_name,
869
 
            b'_new_parent': new_parent,
870
 
            b'_new_executability': new_executability,
871
 
            b'_new_id': new_id,
872
 
            b'_tree_path_ids': tree_path_ids,
873
 
            b'_removed_id': removed_id,
874
 
            b'_removed_contents': removed_contents,
875
 
            b'_non_present_ids': non_present_ids,
876
 
            }
877
 
        yield serializer.bytes_record(bencode.bencode(attribs),
878
 
                                      ((b'attribs',),))
879
 
        for trans_id, kind in sorted(self._new_contents.items()):
880
 
            if kind == 'file':
881
 
                with open(self._limbo_name(trans_id), 'rb') as cur_file:
882
 
                    lines = cur_file.readlines()
883
 
                parents = self._get_parents_lines(trans_id)
884
 
                mpdiff = multiparent.MultiParent.from_lines(lines, parents)
885
 
                content = b''.join(mpdiff.to_patch())
886
 
            if kind == 'directory':
887
 
                content = b''
888
 
            if kind == 'symlink':
889
 
                content = self._read_symlink_target(trans_id)
890
 
                if not isinstance(content, bytes):
891
 
                    content = content.encode('utf-8')
892
 
            yield serializer.bytes_record(
893
 
                content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
894
 
 
895
 
    def deserialize(self, records):
896
 
        """Deserialize a stored TreeTransform.
897
 
 
898
 
        :param records: An iterable of (names, content) tuples, as per
899
 
            pack.ContainerPushParser.
900
 
        """
901
 
        from .. import bencode
902
 
        names, content = next(records)
903
 
        attribs = bencode.bdecode(content)
904
 
        self._id_number = attribs[b'_id_number']
905
 
        self._new_name = {k.decode('utf-8'): v.decode('utf-8')
906
 
                          for k, v in attribs[b'_new_name'].items()}
907
 
        self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
908
 
                            for k, v in attribs[b'_new_parent'].items()}
909
 
        self._new_executability = {
910
 
            k.decode('utf-8'): bool(v)
911
 
            for k, v in attribs[b'_new_executability'].items()}
912
 
        self._new_id = {k.decode('utf-8'): v
913
 
                        for k, v in attribs[b'_new_id'].items()}
914
 
        self._r_new_id = {v: k for k, v in self._new_id.items()}
915
 
        self._tree_path_ids = {}
916
 
        self._tree_id_paths = {}
917
 
        for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
918
 
            path = bytepath.decode('utf-8')
919
 
            trans_id = trans_id.decode('utf-8')
920
 
            self._tree_path_ids[path] = trans_id
921
 
            self._tree_id_paths[trans_id] = path
922
 
        self._removed_id = {trans_id.decode('utf-8')
923
 
                            for trans_id in attribs[b'_removed_id']}
924
 
        self._removed_contents = set(
925
 
            trans_id.decode('utf-8')
926
 
            for trans_id in attribs[b'_removed_contents'])
927
 
        self._non_present_ids = {
928
 
            k: v.decode('utf-8')
929
 
            for k, v in attribs[b'_non_present_ids'].items()}
930
 
        for ((trans_id, kind),), content in records:
931
 
            trans_id = trans_id.decode('utf-8')
932
 
            kind = kind.decode('ascii')
933
 
            if kind == 'file':
934
 
                mpdiff = multiparent.MultiParent.from_patch(content)
935
 
                lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
936
 
                self.create_file(lines, trans_id)
937
 
            if kind == 'directory':
938
 
                self.create_directory(trans_id)
939
 
            if kind == 'symlink':
940
 
                self.create_symlink(content.decode('utf-8'), trans_id)
941
 
 
942
709
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
943
710
        """Schedule creation of a new file.
944
711
 
994
761
        """
995
762
        raise NotImplementedError(self.apply)
996
763
 
 
764
    def cook_conflicts(self, raw_conflicts):
 
765
        """Generate a list of cooked conflicts, sorted by file path"""
 
766
        if not raw_conflicts:
 
767
            return
 
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]))
 
780
            elif c[0] == 'non-directory parent':
 
781
                yield TextConflict(fp.get_path(c[2]))
 
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]))
 
788
            elif c[0] == 'path conflict':
 
789
                yield TextConflict(fp.get_path(c[1]))
 
790
            else:
 
791
                raise AssertionError('unknown conflict %s' % c[0])
 
792
 
997
793
 
998
794
class DiskTreeTransform(TreeTransformBase):
999
795
    """Tree transform storing its contents on disk."""
1204
1000
                path = None
1205
1001
            trace.warning(
1206
1002
                'Unable to create symlink "%s" on this filesystem.' % (path,))
 
1003
            self._symlink_target[trans_id] = target
1207
1004
        # We add symlink to _new_contents even if they are unsupported
1208
1005
        # and not created. These entries are subsequently used to avoid
1209
1006
        # conflicts on platforms that don't support symlink
1228
1025
        handle_orphan = conf.get('transform.orphan_policy')
1229
1026
        handle_orphan(self, trans_id, parent_id)
1230
1027
 
 
1028
    def final_entry(self, trans_id):
 
1029
        is_versioned = self.final_is_versioned(trans_id)
 
1030
        fp = FinalPaths(self)
 
1031
        tree_path = fp.get_path(trans_id)
 
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)
 
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))
 
1039
            if kind == 'directory':
 
1040
                return GitTreeDirectory(
 
1041
                    file_id, self.final_name(trans_id), parent_id=parent_id), is_versioned
 
1042
            executable = mode_is_executable(st.st_mode)
 
1043
            mode = object_mode(kind, executable)
 
1044
            blob = blob_from_path_and_stat(encode_git_path(path), st)
 
1045
            if kind == 'symlink':
 
1046
                return GitTreeSymlink(
 
1047
                    file_id, name, parent_id,
 
1048
                    decode_git_path(blob.data)), is_versioned
 
1049
            elif kind == 'file':
 
1050
                return GitTreeFile(
 
1051
                    file_id, name, executable=executable, parent_id=parent_id,
 
1052
                    git_sha1=blob.id, text_size=len(blob.data)), is_versioned
 
1053
            else:
 
1054
                raise AssertionError(kind)
 
1055
        elif trans_id in self._removed_contents:
 
1056
            return None, None
 
1057
        else:
 
1058
            orig_path = self.tree_path(trans_id)
 
1059
            if orig_path is None:
 
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))
 
1066
            try:
 
1067
                ie = next(self._tree.iter_entries_by_dir(
 
1068
                    specific_files=[orig_path]))[1]
 
1069
                ie.file_id = file_id
 
1070
                ie.parent_id = parent_id
 
1071
                return ie, is_versioned
 
1072
            except StopIteration:
 
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
 
1081
 
 
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)
 
1086
            kind = mode_kind(st.st_mode)
 
1087
            if kind == 'directory':
 
1088
                return None, None
 
1089
            executable = mode_is_executable(st.st_mode)
 
1090
            mode = object_mode(kind, executable)
 
1091
            blob = blob_from_path_and_stat(encode_git_path(path), st)
 
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)
 
1103
            elif kind == 'directory':
 
1104
                return None, None
 
1105
            else:
 
1106
                raise AssertionError(kind)
 
1107
            blob = Blob.from_string(contents)
 
1108
        return blob, mode
 
1109
 
1231
1110
 
1232
1111
class GitTreeTransform(DiskTreeTransform):
1233
1112
    """Represent a tree transformation.
1448
1327
        self._limbo_children_names[parent][filename] = trans_id
1449
1328
        return limbo_name
1450
1329
 
1451
 
    def version_file(self, trans_id, file_id=None):
1452
 
        """Schedule a file to become versioned."""
1453
 
        if file_id is None:
1454
 
            raise ValueError()
1455
 
        unique_add(self._new_id, trans_id, file_id)
1456
 
        unique_add(self._r_new_id, file_id, trans_id)
1457
 
 
1458
1330
    def cancel_versioning(self, trans_id):
1459
1331
        """Undo a previous versioning of a file"""
1460
 
        file_id = self._new_id[trans_id]
1461
 
        del self._new_id[trans_id]
1462
 
        del self._r_new_id[file_id]
 
1332
        self._versioned.remove(trans_id)
1463
1333
 
1464
 
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
 
1334
    def apply(self, no_conflicts=False, _mover=None):
1465
1335
        """Apply all changes to the inventory and filesystem.
1466
1336
 
1467
1337
        If filesystem or inventory conflicts are present, MalformedTransform
1471
1341
 
1472
1342
        :param no_conflicts: if True, the caller guarantees there are no
1473
1343
            conflicts, so no check is made.
1474
 
        :param precomputed_delta: An inventory delta to use instead of
1475
 
            calculating one.
1476
1344
        :param _mover: Supply an alternate FileMover, for testing
1477
1345
        """
1478
1346
        for hook in MutableTree.hooks['pre_transform']:
1481
1349
            self._check_malformed()
1482
1350
        self.rename_count = 0
1483
1351
        with ui.ui_factory.nested_progress_bar() as child_pb:
1484
 
            if precomputed_delta is None:
1485
 
                child_pb.update(gettext('Apply phase'), 0, 2)
1486
 
                changes = self._generate_transform_changes()
1487
 
                offset = 1
1488
 
            else:
1489
 
                changes = [
1490
 
                    (op, np, ie) for (op, np, fid, ie) in precomputed_delta]
1491
 
                offset = 0
 
1352
            child_pb.update(gettext('Apply phase'), 0, 2)
 
1353
            index_changes = self._generate_index_changes()
 
1354
            offset = 1
1492
1355
            if _mover is None:
1493
1356
                mover = _FileMover()
1494
1357
            else:
1503
1366
                raise
1504
1367
            else:
1505
1368
                mover.apply_deletions()
1506
 
        if self.final_file_id(self.root) is None:
1507
 
            changes = [e for e in changes if e[0] != '']
1508
 
        self._tree._apply_transform_delta(changes)
 
1369
        self._tree._apply_index_changes(index_changes)
1509
1370
        self._done = True
1510
1371
        self.finalize()
1511
1372
        return _TransformResults(modified_paths, self.rename_count)
1588
1449
        self._new_contents.clear()
1589
1450
        return modified_paths
1590
1451
 
1591
 
    def _inventory_altered(self):
1592
 
        """Determine which trans_ids need new Inventory entries.
1593
 
 
1594
 
        An new entry is needed when anything that would be reflected by an
1595
 
        inventory entry changes, including file name, file_id, parent file_id,
1596
 
        file kind, and the execute bit.
1597
 
 
1598
 
        Some care is taken to return entries with real changes, not cases
1599
 
        where the value is deleted and then restored to its original value,
1600
 
        but some actually unchanged values may be returned.
1601
 
 
1602
 
        :returns: A list of (path, trans_id) for all items requiring an
1603
 
            inventory change. Ordered by path.
1604
 
        """
 
1452
    def _generate_index_changes(self):
 
1453
        """Generate an inventory delta for the current transform."""
 
1454
        removed_id = set(self._removed_id)
 
1455
        removed_id.update(self._removed_contents)
 
1456
        changes = {}
1605
1457
        changed_ids = set()
1606
 
        # Find entries whose file_ids are new (or changed).
1607
 
        new_file_id = set(t for t in self._new_id
1608
 
                          if self._new_id[t] != self.tree_file_id(t))
1609
 
        for id_set in [self._new_name, self._new_parent, new_file_id,
 
1458
        for id_set in [self._new_name, self._new_parent,
1610
1459
                       self._new_executability]:
1611
1460
            changed_ids.update(id_set)
1612
 
        # removing implies a kind change
1613
 
        changed_kind = set(self._removed_contents)
 
1461
        for id_set in [self._new_name, self._new_parent]:
 
1462
            removed_id.update(id_set)
1614
1463
        # so does adding
1615
 
        changed_kind.intersection_update(self._new_contents)
 
1464
        changed_kind = set(self._new_contents)
1616
1465
        # Ignore entries that are already known to have changed.
1617
1466
        changed_kind.difference_update(changed_ids)
1618
1467
        #  to keep only the truly changed ones
1619
1468
        changed_kind = (t for t in changed_kind
1620
1469
                        if self.tree_kind(t) != self.final_kind(t))
1621
 
        # all kind changes will alter the inventory
1622
1470
        changed_ids.update(changed_kind)
1623
 
        # To find entries with changed parent_ids, find parents which existed,
1624
 
        # but changed file_id.
1625
 
        # Now add all their children to the set.
1626
 
        for parent_trans_id in new_file_id:
1627
 
            changed_ids.update(self.iter_tree_children(parent_trans_id))
1628
 
        return sorted(FinalPaths(self).get_paths(changed_ids))
1629
 
 
1630
 
    def _generate_transform_changes(self):
1631
 
        """Generate an inventory delta for the current transform."""
1632
 
        changes = []
1633
 
        new_paths = self._inventory_altered()
1634
 
        total_entries = len(new_paths) + len(self._removed_id)
 
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)
1635
1477
        with ui.ui_factory.nested_progress_bar() as child_pb:
1636
 
            for num, trans_id in enumerate(self._removed_id):
 
1478
            for num, trans_id in enumerate(removed_id):
1637
1479
                if (num % 10) == 0:
1638
1480
                    child_pb.update(gettext('removing file'),
1639
1481
                                    num, total_entries)
1640
 
                if trans_id == self._new_root:
1641
 
                    file_id = self._tree.path2id('')
1642
 
                else:
1643
 
                    file_id = self.tree_file_id(trans_id)
1644
 
                # File-id isn't really being deleted, just moved
1645
 
                if file_id in self._r_new_id:
 
1482
                try:
 
1483
                    path = self._tree_id_paths[trans_id]
 
1484
                except KeyError:
1646
1485
                    continue
1647
 
                path = self._tree_id_paths[trans_id]
1648
 
                changes.append((path, None, None))
1649
 
            new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1650
 
                                     new_paths)
 
1486
                changes[path] = (None, None, None, None)
1651
1487
            for num, (path, trans_id) in enumerate(new_paths):
1652
1488
                if (num % 10) == 0:
1653
1489
                    child_pb.update(gettext('adding file'),
1654
 
                                    num + len(self._removed_id), total_entries)
1655
 
                file_id = new_path_file_ids[trans_id]
1656
 
                if file_id is None:
1657
 
                    continue
 
1490
                                    num + len(removed_id), total_entries)
 
1491
 
1658
1492
                kind = self.final_kind(trans_id)
1659
1493
                if kind is None:
1660
 
                    kind = self._tree.stored_kind(self._tree.id2path(file_id))
1661
 
                parent_trans_id = self.final_parent(trans_id)
1662
 
                parent_file_id = new_path_file_ids.get(parent_trans_id)
1663
 
                if parent_file_id is None:
1664
 
                    parent_file_id = self.final_file_id(parent_trans_id)
1665
 
                if trans_id in self._new_reference_revision:
1666
 
                    new_entry = inventory.TreeReference(
1667
 
                        file_id,
1668
 
                        self._new_name[trans_id],
1669
 
                        self.final_file_id(self._new_parent[trans_id]),
1670
 
                        None, self._new_reference_revision[trans_id])
1671
 
                else:
1672
 
                    new_entry = inventory.make_entry(kind,
1673
 
                                                     self.final_name(trans_id),
1674
 
                                                     parent_file_id, file_id)
1675
 
                try:
1676
 
                    old_path = self._tree.id2path(new_entry.file_id)
1677
 
                except errors.NoSuchId:
1678
 
                    old_path = None
1679
 
                new_executability = self._new_executability.get(trans_id)
1680
 
                if new_executability is not None:
1681
 
                    new_entry.executable = new_executability
1682
 
                changes.append(
1683
 
                    (old_path, path, new_entry))
1684
 
        return changes
 
1494
                    continue
 
1495
                versioned = self.final_is_versioned(trans_id)
 
1496
                if not versioned:
 
1497
                    continue
 
1498
                executability = self._new_executability.get(trans_id)
 
1499
                reference_revision = self._new_reference_revision.get(trans_id)
 
1500
                symlink_target = self._symlink_target.get(trans_id)
 
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()]
 
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):
 
1523
        path = self.tree_path(trans_id)
 
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:
 
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:
 
1551
            return
 
1552
 
 
1553
    def new_orphan(self, trans_id, parent_id):
 
1554
        raise NotImplementedError(self.new_orphan)
 
1555
 
 
1556
 
 
1557
class GitPreviewTree(PreviewTree, GitTree):
 
1558
    """Partial implementation of Tree to support show_diff_trees"""
 
1559
 
 
1560
    def __init__(self, transform):
 
1561
        PreviewTree.__init__(self, transform)
 
1562
        self.store = transform._tree.store
 
1563
        self.mapping = transform._tree.mapping
 
1564
        self._final_paths = FinalPaths(transform)
 
1565
 
 
1566
    def supports_setting_file_ids(self):
 
1567
        return False
 
1568
 
 
1569
    def _supports_executable(self):
 
1570
        return self._transform._limbo_supports_executable()
 
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,
 
1593
                                 versioned_kind))
 
1594
            children.sort()
 
1595
            if parent_path.startswith(prefix):
 
1596
                yield parent_path, children
 
1597
            pending.extend(sorted(subdirs, key=self._final_paths.get_path,
 
1598
                                  reverse=True))
 
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)
 
1621
        if trans_id in self._transform._new_contents:
 
1622
            name = self._transform._limbo_name(trans_id)
 
1623
            return open(name, 'rb')
 
1624
        if trans_id in self._transform._removed_contents:
 
1625
            raise errors.NoSuchFile(path)
 
1626
        orig_path = self._transform.tree_path(trans_id)
 
1627
        return self._transform._tree.get_file(orig_path)
 
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:
 
1643
            return None
 
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)
 
1709
                if not isinstance(link_or_sha1, str):
 
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
 
1731
        if trans_id in self._transform._versioned:
 
1732
            return True
 
1733
        if trans_id in self._transform._removed_id:
 
1734
            return False
 
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.
 
1748
        for trans_id, path in self._list_files_by_dir():
 
1749
            entry, is_versioned = self._transform.final_entry(trans_id)
 
1750
            if entry is None:
 
1751
                continue
 
1752
            if not is_versioned and entry.kind != 'directory':
 
1753
                continue
 
1754
            if specific_files is not None and path not in specific_files:
 
1755
                continue
 
1756
            if entry is not None:
 
1757
                yield path, entry
 
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:
 
1768
                yield trans_id, paths[trans_id][0]
 
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)
 
1776
 
 
1777
    def git_snapshot(self, want_unversioned=False):
 
1778
        extra = set()
 
1779
        os = []
 
1780
        for trans_id, path in self._list_files_by_dir():
 
1781
            if not self._transform.final_is_versioned(trans_id):
 
1782
                if not want_unversioned:
 
1783
                    continue
 
1784
                extra.add(path)
 
1785
            o, mode = self._transform.final_git_entry(trans_id)
 
1786
            if o is not None:
 
1787
                self.store.add_object(o)
 
1788
                os.append((encode_git_path(path), o.id, mode))
 
1789
        if not os:
 
1790
            return None, extra
 
1791
        return commit_tree(self.store, os), extra
 
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)
 
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
 
1801
            if entry is not None:
 
1802
                yield entry