/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: Jelmer Vernooij
  • Date: 2020-08-22 22:46:24 UTC
  • mfrom: (7490.40.105 work)
  • mto: This revision was merged to the branch mainline in revision 7521.
  • Revision ID: jelmer@jelmer.uk-20200822224624-om4a4idsr7cn8jew
merge lp:brz/3.1.

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 conflicts, 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,
38
54
    ReusingTransform,
39
55
    MalformedTransform,
40
56
    )
41
 
from ..bzr.inventorytree import InventoryTreeChange
42
57
 
43
 
from ..bzr import inventory
44
 
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
45
60
 
46
61
 
47
62
class TreeTransformBase(TreeTransform):
59
74
        super(TreeTransformBase, self).__init__(tree, pb=pb)
60
75
        # mapping of trans_id => (sha1 of content, stat_value)
61
76
        self._observed_sha1s = {}
62
 
        # Mapping of trans_id -> new file_id
63
 
        self._new_id = {}
64
 
        # Mapping of old file-id -> trans_id
65
 
        self._non_present_ids = {}
66
 
        # Mapping of new file_id -> trans_id
67
 
        self._r_new_id = {}
 
77
        # Set of versioned trans ids
 
78
        self._versioned = set()
68
79
        # The trans_id that will be used as the tree root
69
 
        if tree.is_versioned(''):
70
 
            self._new_root = self.trans_id_tree_path('')
71
 
        else:
72
 
            self._new_root = None
 
80
        self.root = self.trans_id_tree_path('')
73
81
        # Whether the target is case sensitive
74
82
        self._case_sensitive_target = case_sensitive
 
83
        self._symlink_target = {}
 
84
 
 
85
    @property
 
86
    def mapping(self):
 
87
        return self._tree.mapping
75
88
 
76
89
    def finalize(self):
77
90
        """Release the working tree lock, if held.
86
99
        self._tree.unlock()
87
100
        self._tree = None
88
101
 
89
 
    def __get_root(self):
90
 
        return self._new_root
91
 
 
92
 
    root = property(__get_root)
93
 
 
94
102
    def create_path(self, name, parent):
95
103
        """Assign a transaction id to a new path"""
96
 
        trans_id = self._assign_id()
 
104
        trans_id = self.assign_id()
97
105
        unique_add(self._new_name, trans_id, name)
98
106
        unique_add(self._new_parent, trans_id, parent)
99
107
        return trans_id
100
108
 
101
109
    def adjust_root_path(self, name, parent):
102
110
        """Emulate moving the root by moving all children, instead.
103
 
 
104
 
        We do this by undoing the association of root's transaction id with the
105
 
        current tree.  This allows us to create a new directory with that
106
 
        transaction id.  We unversion the root directory and version the
107
 
        physically new directory, and hope someone versions the tree root
108
 
        later.
109
111
        """
110
 
        old_root = self._new_root
111
 
        old_root_file_id = self.final_file_id(old_root)
112
 
        # force moving all children of root
113
 
        for child_id in self.iter_tree_children(old_root):
114
 
            if child_id != parent:
115
 
                self.adjust_path(self.final_name(child_id),
116
 
                                 self.final_parent(child_id), child_id)
117
 
            file_id = self.final_file_id(child_id)
118
 
            if file_id is not None:
119
 
                self.unversion_file(child_id)
120
 
            self.version_file(child_id, file_id=file_id)
121
 
 
122
 
        # the physical root needs a new transaction id
123
 
        self._tree_path_ids.pop("")
124
 
        self._tree_id_paths.pop(old_root)
125
 
        self._new_root = self.trans_id_tree_path('')
126
 
        if parent == old_root:
127
 
            parent = self._new_root
128
 
        self.adjust_path(name, parent, old_root)
129
 
        self.create_directory(old_root)
130
 
        self.version_file(old_root, file_id=old_root_file_id)
131
 
        self.unversion_file(self._new_root)
132
112
 
133
113
    def fixup_new_roots(self):
134
114
        """Reinterpret requests to change the root directory
148
128
            return
149
129
        if len(new_roots) != 1:
150
130
            raise ValueError('A tree cannot have two roots!')
151
 
        if self._new_root is None:
152
 
            self._new_root = new_roots[0]
153
 
            return
154
131
        old_new_root = new_roots[0]
155
132
        # unversion the new root's directory.
156
 
        if self.final_kind(self._new_root) is None:
157
 
            file_id = self.final_file_id(old_new_root)
158
 
        else:
159
 
            file_id = self.final_file_id(self._new_root)
160
 
        if old_new_root in self._new_id:
 
133
        if old_new_root in self._versioned:
161
134
            self.cancel_versioning(old_new_root)
162
135
        else:
163
136
            self.unversion_file(old_new_root)
164
 
        # if, at this stage, root still has an old file_id, zap it so we can
165
 
        # stick a new one in.
166
 
        if (self.tree_file_id(self._new_root) is not None
167
 
                and self._new_root not in self._removed_id):
168
 
            self.unversion_file(self._new_root)
169
 
        if file_id is not None:
170
 
            self.version_file(self._new_root, file_id=file_id)
171
137
 
172
138
        # Now move children of new root into old root directory.
173
139
        # Ensure all children are registered with the transaction, but don't
175
141
        list(self.iter_tree_children(old_new_root))
176
142
        # Move all children of new root into old root directory.
177
143
        for child in self.by_parent().get(old_new_root, []):
178
 
            self.adjust_path(self.final_name(child), self._new_root, child)
 
144
            self.adjust_path(self.final_name(child), self.root, child)
179
145
 
180
146
        # Ensure old_new_root has no directory.
181
147
        if old_new_root in self._new_contents:
184
150
            self.delete_contents(old_new_root)
185
151
 
186
152
        # prevent deletion of root directory.
187
 
        if self._new_root in self._removed_contents:
188
 
            self.cancel_deletion(self._new_root)
 
153
        if self.root in self._removed_contents:
 
154
            self.cancel_deletion(self.root)
189
155
 
190
156
        # destroy path info for old_new_root.
191
157
        del self._new_parent[old_new_root]
199
165
        """
200
166
        if file_id is None:
201
167
            raise ValueError('None is not a valid file id')
202
 
        if file_id in self._r_new_id and self._r_new_id[file_id] is not None:
203
 
            return self._r_new_id[file_id]
204
 
        else:
205
 
            try:
206
 
                path = self._tree.id2path(file_id)
207
 
            except errors.NoSuchId:
208
 
                if file_id in self._non_present_ids:
209
 
                    return self._non_present_ids[file_id]
210
 
                else:
211
 
                    trans_id = self._assign_id()
212
 
                    self._non_present_ids[file_id] = trans_id
213
 
                    return trans_id
214
 
            else:
215
 
                return self.trans_id_tree_path(path)
 
168
        path = self.mapping.parse_file_id(file_id)
 
169
        return self.trans_id_tree_path(path)
216
170
 
217
171
    def version_file(self, trans_id, file_id=None):
218
172
        """Schedule a file to become versioned."""
219
 
        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)
220
176
 
221
177
    def cancel_versioning(self, trans_id):
222
178
        """Undo a previous versioning of a file"""
233
189
            stale_ids = self._needs_rename.difference(self._new_name)
234
190
            stale_ids.difference_update(self._new_parent)
235
191
            stale_ids.difference_update(self._new_contents)
236
 
            stale_ids.difference_update(self._new_id)
 
192
            stale_ids.difference_update(self._versioned)
237
193
            needs_rename = self._needs_rename.difference(stale_ids)
238
194
            id_sets = (needs_rename, self._new_executability)
239
195
        else:
240
196
            id_sets = (self._new_name, self._new_parent, self._new_contents,
241
 
                       self._new_id, self._new_executability)
 
197
                       self._versioned, self._new_executability)
242
198
        for id_set in id_sets:
243
199
            new_ids.update(id_set)
244
200
        return sorted(FinalPaths(self).get_paths(new_ids))
245
201
 
246
 
    def tree_file_id(self, trans_id):
247
 
        """Determine the file id associated with the trans_id in the tree"""
248
 
        path = self.tree_path(trans_id)
249
 
        if path is None:
250
 
            return None
251
 
        # the file is old; the old id is still valid
252
 
        if self._new_root == trans_id:
253
 
            return self._tree.path2id('')
254
 
        return self._tree.path2id(path)
255
 
 
256
202
    def final_is_versioned(self, trans_id):
257
 
        return self.final_file_id(trans_id) is not None
258
 
 
259
 
    def final_file_id(self, trans_id):
260
 
        """Determine the file id after any changes are applied, or None.
261
 
 
262
 
        None indicates that the file will not be versioned after changes are
263
 
        applied.
264
 
        """
265
 
        try:
266
 
            return self._new_id[trans_id]
267
 
        except KeyError:
268
 
            if trans_id in self._removed_id:
269
 
                return None
270
 
        return self.tree_file_id(trans_id)
271
 
 
272
 
    def inactive_file_id(self, trans_id):
273
 
        """Return the inactive file_id associated with a transaction id.
274
 
        That is, the one in the tree or in non_present_ids.
275
 
        The file_id may actually be active, too.
276
 
        """
277
 
        file_id = self.tree_file_id(trans_id)
278
 
        if file_id is not None:
279
 
            return file_id
280
 
        for key, value in self._non_present_ids.items():
281
 
            if value == trans_id:
282
 
                return key
 
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)
283
211
 
284
212
    def find_raw_conflicts(self):
285
213
        """Find any violations of inventory or filesystem invariants"""
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
 
391
322
        However, existing entries with no contents are okay.
392
323
        """
393
324
        conflicts = []
394
 
        for trans_id in self._new_id:
 
325
        for trans_id in self._versioned:
395
326
            kind = self.final_kind(trans_id)
396
327
            if kind == 'symlink' and not self._tree.supports_symlinks():
397
328
                # Ignore symlinks as they are not supported on this platform
597
528
    def _affected_ids(self):
598
529
        """Return the set of transform ids affected by the transform"""
599
530
        trans_ids = set(self._removed_id)
600
 
        trans_ids.update(self._new_id)
 
531
        trans_ids.update(self._versioned)
601
532
        trans_ids.update(self._removed_contents)
602
533
        trans_ids.update(self._new_contents)
603
534
        trans_ids.update(self._new_executability)
605
536
        trans_ids.update(self._new_parent)
606
537
        return trans_ids
607
538
 
608
 
    def _get_file_id_maps(self):
609
 
        """Return mapping of file_ids to trans_ids in the to and from states"""
610
 
        trans_ids = self._affected_ids()
611
 
        from_trans_ids = {}
612
 
        to_trans_ids = {}
613
 
        # Build up two dicts: trans_ids associated with file ids in the
614
 
        # FROM state, vs the TO state.
615
 
        for trans_id in trans_ids:
616
 
            from_file_id = self.tree_file_id(trans_id)
617
 
            if from_file_id is not None:
618
 
                from_trans_ids[from_file_id] = trans_id
619
 
            to_file_id = self.final_file_id(trans_id)
620
 
            if to_file_id is not None:
621
 
                to_trans_ids[to_file_id] = trans_id
622
 
        return from_trans_ids, to_trans_ids
623
 
 
624
 
    def _from_file_data(self, from_trans_id, from_versioned, from_path):
625
 
        """Get data about a file in the from (tree) state
626
 
 
627
 
        Return a (name, parent, kind, executable) tuple
628
 
        """
629
 
        from_path = self._tree_id_paths.get(from_trans_id)
630
 
        if from_versioned:
631
 
            # get data from working tree if versioned
632
 
            from_entry = next(self._tree.iter_entries_by_dir(
633
 
                specific_files=[from_path]))[1]
634
 
            from_name = from_entry.name
635
 
            from_parent = from_entry.parent_id
636
 
        else:
637
 
            from_entry = None
638
 
            if from_path is None:
639
 
                # File does not exist in FROM state
640
 
                from_name = None
641
 
                from_parent = None
642
 
            else:
643
 
                # File exists, but is not versioned.  Have to use path-
644
 
                # splitting stuff
645
 
                from_name = os.path.basename(from_path)
646
 
                tree_parent = self.get_tree_parent(from_trans_id)
647
 
                from_parent = self.tree_file_id(tree_parent)
648
 
        if from_path is not None:
649
 
            from_kind, from_executable, from_stats = \
650
 
                self._tree._comparison_data(from_entry, from_path)
651
 
        else:
652
 
            from_kind = None
653
 
            from_executable = False
654
 
        return from_name, from_parent, from_kind, from_executable
655
 
 
656
 
    def _to_file_data(self, to_trans_id, from_trans_id, from_executable):
657
 
        """Get data about a file in the to (target) state
658
 
 
659
 
        Return a (name, parent, kind, executable) tuple
660
 
        """
661
 
        to_name = self.final_name(to_trans_id)
662
 
        to_kind = self.final_kind(to_trans_id)
663
 
        to_parent = self.final_file_id(self.final_parent(to_trans_id))
664
 
        if to_trans_id in self._new_executability:
665
 
            to_executable = self._new_executability[to_trans_id]
666
 
        elif to_trans_id == from_trans_id:
667
 
            to_executable = from_executable
668
 
        else:
669
 
            to_executable = False
670
 
        return to_name, to_parent, to_kind, to_executable
671
 
 
672
 
    def iter_changes(self):
 
539
    def iter_changes(self, want_unversioned=False):
673
540
        """Produce output in the same format as Tree.iter_changes.
674
541
 
675
542
        Will produce nonsensical results if invoked while inventory/filesystem
676
543
        conflicts (as reported by TreeTransform.find_raw_conflicts()) are present.
677
 
 
678
 
        This reads the Transform, but only reproduces changes involving a
679
 
        file_id.  Files that are not versioned in either of the FROM or TO
680
 
        states are not reflected.
681
544
        """
682
545
        final_paths = FinalPaths(self)
683
 
        from_trans_ids, to_trans_ids = self._get_file_id_maps()
 
546
        trans_ids = self._affected_ids()
684
547
        results = []
685
 
        # Now iterate through all active file_ids
686
 
        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)
687
551
            modified = False
688
 
            from_trans_id = from_trans_ids.get(file_id)
689
552
            # find file ids, and determine versioning state
690
 
            if from_trans_id is None:
 
553
            if from_path is None:
691
554
                from_versioned = False
692
 
                from_trans_id = to_trans_ids[file_id]
693
555
            else:
694
 
                from_versioned = True
695
 
            to_trans_id = to_trans_ids.get(file_id)
696
 
            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:
697
561
                to_versioned = False
698
 
                to_trans_id = from_trans_id
699
 
            else:
700
 
                to_versioned = True
701
 
 
702
 
            if not from_versioned:
703
 
                from_path = None
704
 
            else:
705
 
                from_path = self._tree_id_paths.get(from_trans_id)
706
 
            if not to_versioned:
 
562
            else:
 
563
                to_versioned = self.final_is_versioned(trans_id)
 
564
            if not want_unversioned and not to_versioned:
707
565
                to_path = None
708
 
            else:
709
 
                to_path = final_paths.get_path(to_trans_id)
710
 
 
711
 
            from_name, from_parent, from_kind, from_executable = \
712
 
                self._from_file_data(from_trans_id, from_versioned, from_path)
713
 
 
714
 
            to_name, to_parent, to_kind, to_executable = \
715
 
                self._to_file_data(to_trans_id, from_trans_id, from_executable)
716
 
 
717
 
            if from_kind != to_kind:
 
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:
718
596
                modified = True
719
597
            elif to_kind in ('file', 'symlink') and (
720
 
                    to_trans_id != from_trans_id
721
 
                    or to_trans_id in self._new_contents):
 
598
                    trans_id in self._new_contents):
722
599
                modified = True
723
600
            if (not modified and from_versioned == to_versioned
724
 
                and from_parent == to_parent and from_name == to_name
 
601
                and from_path == to_path
 
602
                and from_name == to_name
725
603
                    and from_executable == to_executable):
726
604
                continue
 
605
            if (from_path, to_path) == (None, None):
 
606
                continue
727
607
            results.append(
728
 
                InventoryTreeChange(
729
 
                    file_id, (from_path, to_path), modified,
 
608
                TreeChange(
 
609
                    (from_path, to_path), modified,
730
610
                    (from_versioned, to_versioned),
731
 
                    (from_parent, to_parent),
732
611
                    (from_name, to_name),
733
612
                    (from_kind, to_kind),
734
613
                    (from_executable, to_executable)))
743
622
        The tree is a snapshot, and altering the TreeTransform will invalidate
744
623
        it.
745
624
        """
746
 
        raise NotImplementedError(self.get_preview_tree)
 
625
        return GitPreviewTree(self)
747
626
 
748
627
    def commit(self, branch, message, merge_parents=None, strict=False,
749
628
               timestamp=None, timezone=None, committer=None, authors=None,
770
649
        """
771
650
        self._check_malformed()
772
651
        if strict:
773
 
            unversioned = set(self._new_contents).difference(set(self._new_id))
 
652
            unversioned = set(self._new_contents).difference(set(self._versioned))
774
653
            for trans_id in unversioned:
775
654
                if not self.final_is_versioned(trans_id):
776
655
                    raise errors.StrictCommitFailed()
827
706
            return ()
828
707
        return (self._tree.get_file_lines(path),)
829
708
 
830
 
    def serialize(self, serializer):
831
 
        """Serialize this TreeTransform.
832
 
 
833
 
        :param serializer: A Serialiser like pack.ContainerSerializer.
834
 
        """
835
 
        from .. import bencode
836
 
        new_name = {k.encode('utf-8'): v.encode('utf-8')
837
 
                    for k, v in self._new_name.items()}
838
 
        new_parent = {k.encode('utf-8'): v.encode('utf-8')
839
 
                      for k, v in self._new_parent.items()}
840
 
        new_id = {k.encode('utf-8'): v
841
 
                  for k, v in self._new_id.items()}
842
 
        new_executability = {k.encode('utf-8'): int(v)
843
 
                             for k, v in self._new_executability.items()}
844
 
        tree_path_ids = {k.encode('utf-8'): v.encode('utf-8')
845
 
                         for k, v in self._tree_path_ids.items()}
846
 
        non_present_ids = {k: v.encode('utf-8')
847
 
                           for k, v in self._non_present_ids.items()}
848
 
        removed_contents = [trans_id.encode('utf-8')
849
 
                            for trans_id in self._removed_contents]
850
 
        removed_id = [trans_id.encode('utf-8')
851
 
                      for trans_id in self._removed_id]
852
 
        attribs = {
853
 
            b'_id_number': self._id_number,
854
 
            b'_new_name': new_name,
855
 
            b'_new_parent': new_parent,
856
 
            b'_new_executability': new_executability,
857
 
            b'_new_id': new_id,
858
 
            b'_tree_path_ids': tree_path_ids,
859
 
            b'_removed_id': removed_id,
860
 
            b'_removed_contents': removed_contents,
861
 
            b'_non_present_ids': non_present_ids,
862
 
            }
863
 
        yield serializer.bytes_record(bencode.bencode(attribs),
864
 
                                      ((b'attribs',),))
865
 
        for trans_id, kind in sorted(self._new_contents.items()):
866
 
            if kind == 'file':
867
 
                with open(self._limbo_name(trans_id), 'rb') as cur_file:
868
 
                    lines = cur_file.readlines()
869
 
                parents = self._get_parents_lines(trans_id)
870
 
                mpdiff = multiparent.MultiParent.from_lines(lines, parents)
871
 
                content = b''.join(mpdiff.to_patch())
872
 
            if kind == 'directory':
873
 
                content = b''
874
 
            if kind == 'symlink':
875
 
                content = self._read_symlink_target(trans_id)
876
 
                if not isinstance(content, bytes):
877
 
                    content = content.encode('utf-8')
878
 
            yield serializer.bytes_record(
879
 
                content, ((trans_id.encode('utf-8'), kind.encode('ascii')),))
880
 
 
881
 
    def deserialize(self, records):
882
 
        """Deserialize a stored TreeTransform.
883
 
 
884
 
        :param records: An iterable of (names, content) tuples, as per
885
 
            pack.ContainerPushParser.
886
 
        """
887
 
        from .. import bencode
888
 
        names, content = next(records)
889
 
        attribs = bencode.bdecode(content)
890
 
        self._id_number = attribs[b'_id_number']
891
 
        self._new_name = {k.decode('utf-8'): v.decode('utf-8')
892
 
                          for k, v in attribs[b'_new_name'].items()}
893
 
        self._new_parent = {k.decode('utf-8'): v.decode('utf-8')
894
 
                            for k, v in attribs[b'_new_parent'].items()}
895
 
        self._new_executability = {
896
 
            k.decode('utf-8'): bool(v)
897
 
            for k, v in attribs[b'_new_executability'].items()}
898
 
        self._new_id = {k.decode('utf-8'): v
899
 
                        for k, v in attribs[b'_new_id'].items()}
900
 
        self._r_new_id = {v: k for k, v in self._new_id.items()}
901
 
        self._tree_path_ids = {}
902
 
        self._tree_id_paths = {}
903
 
        for bytepath, trans_id in attribs[b'_tree_path_ids'].items():
904
 
            path = bytepath.decode('utf-8')
905
 
            trans_id = trans_id.decode('utf-8')
906
 
            self._tree_path_ids[path] = trans_id
907
 
            self._tree_id_paths[trans_id] = path
908
 
        self._removed_id = {trans_id.decode('utf-8')
909
 
                            for trans_id in attribs[b'_removed_id']}
910
 
        self._removed_contents = set(
911
 
            trans_id.decode('utf-8')
912
 
            for trans_id in attribs[b'_removed_contents'])
913
 
        self._non_present_ids = {
914
 
            k: v.decode('utf-8')
915
 
            for k, v in attribs[b'_non_present_ids'].items()}
916
 
        for ((trans_id, kind),), content in records:
917
 
            trans_id = trans_id.decode('utf-8')
918
 
            kind = kind.decode('ascii')
919
 
            if kind == 'file':
920
 
                mpdiff = multiparent.MultiParent.from_patch(content)
921
 
                lines = mpdiff.to_lines(self._get_parents_texts(trans_id))
922
 
                self.create_file(lines, trans_id)
923
 
            if kind == 'directory':
924
 
                self.create_directory(trans_id)
925
 
            if kind == 'symlink':
926
 
                self.create_symlink(content.decode('utf-8'), trans_id)
927
 
 
928
709
    def create_file(self, contents, trans_id, mode_id=None, sha1=None):
929
710
        """Schedule creation of a new file.
930
711
 
996
777
            elif c[0] == 'missing parent':
997
778
                # TODO(jelmer): This should not make it to here
998
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]))
999
790
            else:
1000
791
                raise AssertionError('unknown conflict %s' % c[0])
1001
792
 
1209
1000
                path = None
1210
1001
            trace.warning(
1211
1002
                'Unable to create symlink "%s" on this filesystem.' % (path,))
 
1003
            self._symlink_target[trans_id] = target
1212
1004
        # We add symlink to _new_contents even if they are unsupported
1213
1005
        # and not created. These entries are subsequently used to avoid
1214
1006
        # conflicts on platforms that don't support symlink
1233
1025
        handle_orphan = conf.get('transform.orphan_policy')
1234
1026
        handle_orphan(self, trans_id, parent_id)
1235
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
 
1236
1110
 
1237
1111
class GitTreeTransform(DiskTreeTransform):
1238
1112
    """Represent a tree transformation.
1453
1327
        self._limbo_children_names[parent][filename] = trans_id
1454
1328
        return limbo_name
1455
1329
 
1456
 
    def version_file(self, trans_id, file_id=None):
1457
 
        """Schedule a file to become versioned."""
1458
 
        if file_id is None:
1459
 
            raise ValueError()
1460
 
        unique_add(self._new_id, trans_id, file_id)
1461
 
        unique_add(self._r_new_id, file_id, trans_id)
1462
 
 
1463
1330
    def cancel_versioning(self, trans_id):
1464
1331
        """Undo a previous versioning of a file"""
1465
 
        file_id = self._new_id[trans_id]
1466
 
        del self._new_id[trans_id]
1467
 
        del self._r_new_id[file_id]
 
1332
        self._versioned.remove(trans_id)
1468
1333
 
1469
 
    def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None):
 
1334
    def apply(self, no_conflicts=False, _mover=None):
1470
1335
        """Apply all changes to the inventory and filesystem.
1471
1336
 
1472
1337
        If filesystem or inventory conflicts are present, MalformedTransform
1476
1341
 
1477
1342
        :param no_conflicts: if True, the caller guarantees there are no
1478
1343
            conflicts, so no check is made.
1479
 
        :param precomputed_delta: An inventory delta to use instead of
1480
 
            calculating one.
1481
1344
        :param _mover: Supply an alternate FileMover, for testing
1482
1345
        """
1483
1346
        for hook in MutableTree.hooks['pre_transform']:
1486
1349
            self._check_malformed()
1487
1350
        self.rename_count = 0
1488
1351
        with ui.ui_factory.nested_progress_bar() as child_pb:
1489
 
            if precomputed_delta is None:
1490
 
                child_pb.update(gettext('Apply phase'), 0, 2)
1491
 
                changes = self._generate_transform_changes()
1492
 
                offset = 1
1493
 
            else:
1494
 
                changes = [
1495
 
                    (op, np, ie) for (op, np, fid, ie) in precomputed_delta]
1496
 
                offset = 0
 
1352
            child_pb.update(gettext('Apply phase'), 0, 2)
 
1353
            index_changes = self._generate_index_changes()
 
1354
            offset = 1
1497
1355
            if _mover is None:
1498
1356
                mover = _FileMover()
1499
1357
            else:
1508
1366
                raise
1509
1367
            else:
1510
1368
                mover.apply_deletions()
1511
 
        if self.final_file_id(self.root) is None:
1512
 
            changes = [e for e in changes if e[0] != '']
1513
 
        self._tree._apply_transform_delta(changes)
 
1369
        self._tree._apply_index_changes(index_changes)
1514
1370
        self._done = True
1515
1371
        self.finalize()
1516
1372
        return _TransformResults(modified_paths, self.rename_count)
1593
1449
        self._new_contents.clear()
1594
1450
        return modified_paths
1595
1451
 
1596
 
    def _inventory_altered(self):
1597
 
        """Determine which trans_ids need new Inventory entries.
1598
 
 
1599
 
        An new entry is needed when anything that would be reflected by an
1600
 
        inventory entry changes, including file name, file_id, parent file_id,
1601
 
        file kind, and the execute bit.
1602
 
 
1603
 
        Some care is taken to return entries with real changes, not cases
1604
 
        where the value is deleted and then restored to its original value,
1605
 
        but some actually unchanged values may be returned.
1606
 
 
1607
 
        :returns: A list of (path, trans_id) for all items requiring an
1608
 
            inventory change. Ordered by path.
1609
 
        """
 
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 = {}
1610
1457
        changed_ids = set()
1611
 
        # Find entries whose file_ids are new (or changed).
1612
 
        new_file_id = set(t for t in self._new_id
1613
 
                          if self._new_id[t] != self.tree_file_id(t))
1614
 
        for id_set in [self._new_name, self._new_parent, new_file_id,
 
1458
        for id_set in [self._new_name, self._new_parent,
1615
1459
                       self._new_executability]:
1616
1460
            changed_ids.update(id_set)
1617
 
        # removing implies a kind change
1618
 
        changed_kind = set(self._removed_contents)
 
1461
        for id_set in [self._new_name, self._new_parent]:
 
1462
            removed_id.update(id_set)
1619
1463
        # so does adding
1620
 
        changed_kind.intersection_update(self._new_contents)
 
1464
        changed_kind = set(self._new_contents)
1621
1465
        # Ignore entries that are already known to have changed.
1622
1466
        changed_kind.difference_update(changed_ids)
1623
1467
        #  to keep only the truly changed ones
1624
1468
        changed_kind = (t for t in changed_kind
1625
1469
                        if self.tree_kind(t) != self.final_kind(t))
1626
 
        # all kind changes will alter the inventory
1627
1470
        changed_ids.update(changed_kind)
1628
 
        # To find entries with changed parent_ids, find parents which existed,
1629
 
        # but changed file_id.
1630
 
        # Now add all their children to the set.
1631
 
        for parent_trans_id in new_file_id:
1632
 
            changed_ids.update(self.iter_tree_children(parent_trans_id))
1633
 
        return sorted(FinalPaths(self).get_paths(changed_ids))
1634
 
 
1635
 
    def _generate_transform_changes(self):
1636
 
        """Generate an inventory delta for the current transform."""
1637
 
        changes = []
1638
 
        new_paths = self._inventory_altered()
1639
 
        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)
1640
1477
        with ui.ui_factory.nested_progress_bar() as child_pb:
1641
 
            for num, trans_id in enumerate(self._removed_id):
 
1478
            for num, trans_id in enumerate(removed_id):
1642
1479
                if (num % 10) == 0:
1643
1480
                    child_pb.update(gettext('removing file'),
1644
1481
                                    num, total_entries)
1645
 
                if trans_id == self._new_root:
1646
 
                    file_id = self._tree.path2id('')
1647
 
                else:
1648
 
                    file_id = self.tree_file_id(trans_id)
1649
 
                # File-id isn't really being deleted, just moved
1650
 
                if file_id in self._r_new_id:
 
1482
                try:
 
1483
                    path = self._tree_id_paths[trans_id]
 
1484
                except KeyError:
1651
1485
                    continue
1652
 
                path = self._tree_id_paths[trans_id]
1653
 
                changes.append((path, None, None))
1654
 
            new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in
1655
 
                                     new_paths)
 
1486
                changes[path] = (None, None, None, None)
1656
1487
            for num, (path, trans_id) in enumerate(new_paths):
1657
1488
                if (num % 10) == 0:
1658
1489
                    child_pb.update(gettext('adding file'),
1659
 
                                    num + len(self._removed_id), total_entries)
1660
 
                file_id = new_path_file_ids[trans_id]
1661
 
                if file_id is None:
1662
 
                    continue
 
1490
                                    num + len(removed_id), total_entries)
 
1491
 
1663
1492
                kind = self.final_kind(trans_id)
1664
1493
                if kind is None:
1665
 
                    kind = self._tree.stored_kind(self._tree.id2path(file_id))
1666
 
                parent_trans_id = self.final_parent(trans_id)
1667
 
                parent_file_id = new_path_file_ids.get(parent_trans_id)
1668
 
                if parent_file_id is None:
1669
 
                    parent_file_id = self.final_file_id(parent_trans_id)
1670
 
                if trans_id in self._new_reference_revision:
1671
 
                    new_entry = inventory.TreeReference(
1672
 
                        file_id,
1673
 
                        self._new_name[trans_id],
1674
 
                        self.final_file_id(self._new_parent[trans_id]),
1675
 
                        None, self._new_reference_revision[trans_id])
1676
 
                else:
1677
 
                    new_entry = inventory.make_entry(kind,
1678
 
                                                     self.final_name(trans_id),
1679
 
                                                     parent_file_id, file_id)
1680
 
                try:
1681
 
                    old_path = self._tree.id2path(new_entry.file_id)
1682
 
                except errors.NoSuchId:
1683
 
                    old_path = None
1684
 
                new_executability = self._new_executability.get(trans_id)
1685
 
                if new_executability is not None:
1686
 
                    new_entry.executable = new_executability
1687
 
                changes.append(
1688
 
                    (old_path, path, new_entry))
1689
 
        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