/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/tree.py

  • Committer: Jelmer Vernooij
  • Date: 2020-07-05 12:50:01 UTC
  • mfrom: (7490.40.46 work)
  • mto: (7490.40.48 work)
  • mto: This revision was merged to the branch mainline in revision 7519.
  • Revision ID: jelmer@jelmer.uk-20200705125001-7s3vo0p55szbbws7
Merge lp:brz/3.1.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
"""Git Trees."""
19
19
 
 
20
from __future__ import absolute_import
 
21
 
20
22
from collections import deque
21
23
import errno
22
24
from io import BytesIO
64
66
    CURRENT_REVISION,
65
67
    NULL_REVISION,
66
68
    )
 
69
from ..sixish import (
 
70
    text_type,
 
71
    viewitems,
 
72
    )
67
73
 
68
74
from .mapping import (
69
75
    encode_git_path,
76
82
    TransportObjectStore,
77
83
    TransportRepo,
78
84
    )
79
 
from ..bzr.inventorytree import InventoryTreeChange
80
85
 
81
86
 
82
87
class GitTreeDirectory(_mod_tree.TreeDirectory):
83
88
 
84
 
    __slots__ = ['file_id', 'name', 'parent_id']
 
89
    __slots__ = ['file_id', 'name', 'parent_id', 'children']
85
90
 
86
91
    def __init__(self, file_id, name, parent_id):
87
92
        self.file_id = file_id
88
93
        self.name = name
89
94
        self.parent_id = parent_id
 
95
        # TODO(jelmer)
 
96
        self.children = {}
90
97
 
91
98
    @property
92
99
    def kind(self):
114
121
 
115
122
class GitTreeFile(_mod_tree.TreeFile):
116
123
 
117
 
    __slots__ = ['file_id', 'name', 'parent_id', 'text_size',
118
 
                 'executable', 'git_sha1']
 
124
    __slots__ = ['file_id', 'name', 'parent_id', 'text_size', 'text_sha1',
 
125
                 'executable']
119
126
 
120
127
    def __init__(self, file_id, name, parent_id, text_size=None,
121
 
                 git_sha1=None, executable=None):
 
128
                 text_sha1=None, executable=None):
122
129
        self.file_id = file_id
123
130
        self.name = name
124
131
        self.parent_id = parent_id
125
132
        self.text_size = text_size
126
 
        self.git_sha1 = git_sha1
 
133
        self.text_sha1 = text_sha1
127
134
        self.executable = executable
128
135
 
129
136
    @property
135
142
                self.file_id == other.file_id and
136
143
                self.name == other.name and
137
144
                self.parent_id == other.parent_id and
138
 
                self.git_sha1 == other.git_sha1 and
 
145
                self.text_sha1 == other.text_sha1 and
139
146
                self.text_size == other.text_size and
140
147
                self.executable == other.executable)
141
148
 
142
149
    def __repr__(self):
143
150
        return ("%s(file_id=%r, name=%r, parent_id=%r, text_size=%r, "
144
 
                "git_sha1=%r, executable=%r)") % (
 
151
                "text_sha1=%r, executable=%r)") % (
145
152
            type(self).__name__, self.file_id, self.name, self.parent_id,
146
 
            self.text_size, self.git_sha1, self.executable)
 
153
            self.text_size, self.text_sha1, self.executable)
147
154
 
148
155
    def copy(self):
149
156
        ret = self.__class__(
150
157
            self.file_id, self.name, self.parent_id)
151
 
        ret.git_sha1 = self.git_sha1
 
158
        ret.text_sha1 = self.text_sha1
152
159
        ret.text_size = self.text_size
153
160
        ret.executable = self.executable
154
161
        return ret
256
263
    return path
257
264
 
258
265
 
259
 
class GitTree(_mod_tree.Tree):
260
 
 
261
 
    def iter_git_objects(self):
262
 
        """Iterate over all the objects in the tree.
263
 
 
264
 
        :return :Yields tuples with (path, sha, mode)
265
 
        """
266
 
        raise NotImplementedError(self.iter_git_objects)
267
 
 
268
 
    def git_snapshot(self, want_unversioned=False):
269
 
        """Snapshot a tree, and return tree object.
270
 
 
271
 
        :return: Tree sha and set of extras
272
 
        """
273
 
        raise NotImplementedError(self.snapshot)
274
 
 
275
 
    def preview_transform(self, pb=None):
276
 
        from .transform import GitTransformPreview
277
 
        return GitTransformPreview(self, pb=pb)
278
 
 
279
 
    def find_related_paths_across_trees(self, paths, trees=[],
280
 
                                        require_versioned=True):
281
 
        if paths is None:
282
 
            return None
283
 
        if require_versioned:
284
 
            trees = [self] + (trees if trees is not None else [])
285
 
            unversioned = set()
286
 
            for p in paths:
287
 
                for t in trees:
288
 
                    if t.is_versioned(p):
289
 
                        break
290
 
                else:
291
 
                    unversioned.add(p)
292
 
            if unversioned:
293
 
                raise errors.PathsNotVersionedError(unversioned)
294
 
        return filter(self.is_versioned, paths)
295
 
 
296
 
    def _submodule_info(self):
297
 
        if self._submodules is None:
298
 
            try:
299
 
                with self.get_file('.gitmodules') as f:
300
 
                    config = GitConfigFile.from_file(f)
301
 
                    self._submodules = {
302
 
                        path: (url, section)
303
 
                        for path, url, section in parse_submodules(config)}
304
 
            except errors.NoSuchFile:
305
 
                self._submodules = {}
306
 
        return self._submodules
307
 
 
308
 
 
309
 
class GitRevisionTree(revisiontree.RevisionTree, GitTree):
 
266
class GitRevisionTree(revisiontree.RevisionTree):
310
267
    """Revision tree implementation based on Git objects."""
311
268
 
312
269
    def __init__(self, repository, revision_id):
328
285
                raise errors.NoSuchRevision(repository, revision_id)
329
286
            self.tree = commit.tree
330
287
 
331
 
    def git_snapshot(self, want_unversioned=False):
332
 
        return self.tree, set()
 
288
    def _submodule_info(self):
 
289
        if self._submodules is None:
 
290
            try:
 
291
                with self.get_file('.gitmodules') as f:
 
292
                    config = GitConfigFile.from_file(f)
 
293
                    self._submodules = {
 
294
                        path: (url, section)
 
295
                        for path, url, section in parse_submodules(config)}
 
296
            except errors.NoSuchFile:
 
297
                self._submodules = {}
 
298
        return self._submodules
333
299
 
334
300
    def _get_submodule_repository(self, relpath):
335
301
        if not isinstance(relpath, bytes):
461
427
        else:
462
428
            return True
463
429
 
 
430
    def _submodule_info(self):
 
431
        if self._submodules is None:
 
432
            try:
 
433
                with self.get_file('.gitmodules') as f:
 
434
                    config = GitConfigFile.from_file(f)
 
435
                    self._submodules = {
 
436
                        path: (url, section)
 
437
                        for path, url, section in parse_submodules(config)}
 
438
            except errors.NoSuchFile:
 
439
                self._submodules = {}
 
440
        return self._submodules
 
441
 
464
442
    def list_files(self, include_root=False, from_dir=None, recursive=True,
465
443
                   recurse_nested=False):
466
444
        if self.tree is None:
524
502
            ie.reference_revision = self.mapping.revision_id_foreign_to_bzr(
525
503
                hexsha)
526
504
        else:
527
 
            ie.git_sha1 = hexsha
528
 
            ie.text_size = None
 
505
            data = store[hexsha].data
 
506
            ie.text_sha1 = osutils.sha_string(data)
 
507
            ie.text_size = len(data)
529
508
            ie.executable = mode_is_executable(mode)
530
509
        return ie
531
510
 
676
655
        else:
677
656
            return (kind, None, None, None)
678
657
 
 
658
    def find_related_paths_across_trees(self, paths, trees=[],
 
659
                                        require_versioned=True):
 
660
        if paths is None:
 
661
            return None
 
662
        if require_versioned:
 
663
            trees = [self] + (trees if trees is not None else [])
 
664
            unversioned = set()
 
665
            for p in paths:
 
666
                for t in trees:
 
667
                    if t.is_versioned(p):
 
668
                        break
 
669
                else:
 
670
                    unversioned.add(p)
 
671
            if unversioned:
 
672
                raise errors.PathsNotVersionedError(unversioned)
 
673
        return filter(self.is_versioned, paths)
 
674
 
679
675
    def _iter_tree_contents(self, include_trees=False):
680
676
        if self.tree is None:
681
677
            return iter([])
709
705
    def walkdirs(self, prefix=u""):
710
706
        (store, mode, hexsha) = self._lookup_path(prefix)
711
707
        todo = deque(
712
 
            [(store, encode_git_path(prefix), hexsha)])
 
708
            [(store, encode_git_path(prefix), hexsha, self.path2id(prefix))])
713
709
        while todo:
714
 
            store, path, tree_sha = todo.popleft()
 
710
            store, path, tree_sha, parent_id = todo.popleft()
715
711
            path_decoded = decode_git_path(path)
716
712
            tree = store[tree_sha]
717
713
            children = []
719
715
                if self.mapping.is_special_file(name):
720
716
                    continue
721
717
                child_path = posixpath.join(path, name)
 
718
                file_id = self.path2id(decode_git_path(child_path))
722
719
                if stat.S_ISDIR(mode):
723
 
                    todo.append((store, child_path, hexsha))
 
720
                    todo.append((store, child_path, hexsha, file_id))
724
721
                children.append(
725
722
                    (decode_git_path(child_path), decode_git_path(name),
726
723
                        mode_kind(mode), None,
727
 
                        mode_kind(mode)))
728
 
            yield path_decoded, children
 
724
                        file_id, mode_kind(mode)))
 
725
            yield (path_decoded, parent_id), children
 
726
 
 
727
    def preview_transform(self, pb=None):
 
728
        from ..bzr.transform import TransformPreview
 
729
        return TransformPreview(self, pb=pb)
729
730
 
730
731
 
731
732
def tree_delta_from_git_changes(changes, mappings,
819
820
            newpath = None
820
821
        if oldpath is None and newpath is None:
821
822
            continue
822
 
        change = InventoryTreeChange(
 
823
        change = _mod_tree.TreeChange(
823
824
            fileid, (oldpath_decoded, newpath_decoded), (oldsha != newsha),
824
825
            (oldversioned, newversioned),
825
826
            (oldparent, newparent), (oldname, newname),
865
866
        parent_id = new_mapping.generate_file_id(parent_path)
866
867
        file_id = new_mapping.generate_file_id(path_decoded)
867
868
        ret.added.append(
868
 
            InventoryTreeChange(
 
869
            _mod_tree.TreeChange(
869
870
                file_id, (None, path_decoded), True,
870
871
                (False, True),
871
872
                (None, parent_id),
961
962
            fileid = mapping.generate_file_id(newpath_decoded)
962
963
        else:
963
964
            fileid = None
964
 
        if oldkind == 'directory' and newkind == 'directory':
965
 
            modified = False
966
 
        else:
967
 
            modified = (oldsha != newsha) or (oldmode != newmode)
968
 
        yield InventoryTreeChange(
969
 
            fileid, (oldpath_decoded, newpath_decoded),
970
 
            modified,
 
965
        yield _mod_tree.TreeChange(
 
966
            fileid, (oldpath_decoded, newpath_decoded), (oldsha != newsha),
971
967
            (oldversioned, newversioned),
972
968
            (oldparent, newparent), (oldname, newname),
973
969
            (oldkind, newkind), (oldexe, newexe),
981
977
    _matching_to_tree_format = None
982
978
    _test_mutable_trees_to_test_trees = None
983
979
 
984
 
    def __init__(self, source, target):
985
 
        super(InterGitTrees, self).__init__(source, target)
986
 
        if self.source.store == self.target.store:
987
 
            self.store = self.source.store
988
 
        else:
989
 
            self.store = OverlayObjectStore(
990
 
                [self.source.store, self.target.store])
991
 
        self.rename_detector = RenameDetector(self.store)
992
 
 
993
980
    @classmethod
994
981
    def is_compatible(cls, source, target):
995
 
        return isinstance(source, GitTree) and isinstance(target, GitTree)
 
982
        return (isinstance(source, GitRevisionTree) and
 
983
                isinstance(target, GitRevisionTree))
996
984
 
997
985
    def compare(self, want_unchanged=False, specific_files=None,
998
986
                extra_trees=None, require_versioned=False, include_root=False,
1030
1018
    def _iter_git_changes(self, want_unchanged=False, specific_files=None,
1031
1019
                          require_versioned=False, extra_trees=None,
1032
1020
                          want_unversioned=False, include_trees=True):
1033
 
        trees = [self.source]
1034
 
        if extra_trees is not None:
1035
 
            trees.extend(extra_trees)
1036
 
        if specific_files is not None:
1037
 
            specific_files = self.target.find_related_paths_across_trees(
1038
 
                specific_files, trees,
1039
 
                require_versioned=require_versioned)
1040
 
        # TODO(jelmer): Restrict to specific_files, for performance reasons.
1041
 
        with self.lock_read():
1042
 
            from_tree_sha, from_extras = self.source.git_snapshot(
1043
 
                want_unversioned=want_unversioned)
1044
 
            to_tree_sha, to_extras = self.target.git_snapshot(
1045
 
                want_unversioned=want_unversioned)
1046
 
            changes = tree_changes(
1047
 
                self.store, from_tree_sha, to_tree_sha,
1048
 
                include_trees=include_trees,
1049
 
                rename_detector=self.rename_detector,
1050
 
                want_unchanged=want_unchanged, change_type_same=True)
1051
 
            return changes, from_extras, to_extras
 
1021
        raise NotImplementedError(self._iter_git_changes)
1052
1022
 
1053
1023
    def find_target_path(self, path, recurse='none'):
1054
1024
        ret = self.find_target_paths([path], recurse=recurse)
1103
1073
        return ret
1104
1074
 
1105
1075
 
1106
 
_mod_tree.InterTree.register_optimiser(InterGitTrees)
1107
 
 
1108
 
 
1109
 
class MutableGitIndexTree(mutabletree.MutableTree, GitTree):
 
1076
class InterGitRevisionTrees(InterGitTrees):
 
1077
    """InterTree that works between two git revision trees."""
 
1078
 
 
1079
    _matching_from_tree_format = None
 
1080
    _matching_to_tree_format = None
 
1081
    _test_mutable_trees_to_test_trees = None
 
1082
 
 
1083
    @classmethod
 
1084
    def is_compatible(cls, source, target):
 
1085
        return (isinstance(source, GitRevisionTree) and
 
1086
                isinstance(target, GitRevisionTree))
 
1087
 
 
1088
    def _iter_git_changes(self, want_unchanged=False, specific_files=None,
 
1089
                          require_versioned=True, extra_trees=None,
 
1090
                          want_unversioned=False, include_trees=True):
 
1091
        trees = [self.source]
 
1092
        if extra_trees is not None:
 
1093
            trees.extend(extra_trees)
 
1094
        if specific_files is not None:
 
1095
            specific_files = self.target.find_related_paths_across_trees(
 
1096
                specific_files, trees,
 
1097
                require_versioned=require_versioned)
 
1098
 
 
1099
        if (self.source._repository._git.object_store !=
 
1100
                self.target._repository._git.object_store):
 
1101
            store = OverlayObjectStore(
 
1102
                [self.source._repository._git.object_store,
 
1103
                    self.target._repository._git.object_store])
 
1104
        else:
 
1105
            store = self.source._repository._git.object_store
 
1106
        rename_detector = RenameDetector(store)
 
1107
        changes = tree_changes(
 
1108
            store, self.source.tree, self.target.tree,
 
1109
            want_unchanged=want_unchanged, include_trees=include_trees,
 
1110
            change_type_same=True, rename_detector=rename_detector)
 
1111
        return changes, set(), set()
 
1112
 
 
1113
 
 
1114
_mod_tree.InterTree.register_optimiser(InterGitRevisionTrees)
 
1115
 
 
1116
 
 
1117
class MutableGitIndexTree(mutabletree.MutableTree):
1110
1118
 
1111
1119
    def __init__(self):
1112
1120
        self._lock_mode = None
1115
1123
        self._index_dirty = False
1116
1124
        self._submodules = None
1117
1125
 
1118
 
    def git_snapshot(self, want_unversioned=False):
1119
 
        return snapshot_workingtree(self, want_unversioned=want_unversioned)
1120
 
 
1121
1126
    def is_versioned(self, path):
1122
1127
        with self.lock_read():
1123
1128
            path = encode_git_path(path.rstrip('/'))
1137
1142
        if self._lock_mode is None:
1138
1143
            raise errors.ObjectNotLocked(self)
1139
1144
        self._versioned_dirs = set()
1140
 
        for p, sha, mode in self.iter_git_objects():
 
1145
        for p, i in self._recurse_index_entries():
1141
1146
            self._ensure_versioned_dir(posixpath.dirname(p))
1142
1147
 
1143
1148
    def _ensure_versioned_dir(self, dirname):
1186
1191
    def _read_submodule_head(self, path):
1187
1192
        raise NotImplementedError(self._read_submodule_head)
1188
1193
 
 
1194
    def _submodule_info(self):
 
1195
        if self._submodules is None:
 
1196
            try:
 
1197
                with self.get_file('.gitmodules') as f:
 
1198
                    config = GitConfigFile.from_file(f)
 
1199
                    self._submodules = {
 
1200
                        path: (url, section)
 
1201
                        for path, url, section in parse_submodules(config)}
 
1202
            except errors.NoSuchFile:
 
1203
                self._submodules = {}
 
1204
        return self._submodules
 
1205
 
1189
1206
    def _lookup_index(self, encoded_path):
1190
1207
        if not isinstance(encoded_path, bytes):
1191
1208
            raise TypeError(encoded_path)
1221
1238
        # TODO(jelmer): Keep track of dirty per index
1222
1239
        self._index_dirty = True
1223
1240
 
1224
 
    def _apply_index_changes(self, changes):
1225
 
        for (path, kind, executability, reference_revision,
1226
 
             symlink_target) in changes:
1227
 
            if kind is None or kind == 'directory':
1228
 
                (index, subpath) = self._lookup_index(
1229
 
                    encode_git_path(path))
1230
 
                try:
1231
 
                    self._index_del_entry(index, subpath)
1232
 
                except KeyError:
1233
 
                    pass
1234
 
                else:
1235
 
                    self._versioned_dirs = None
1236
 
            else:
1237
 
                self._index_add_entry(
1238
 
                    path, kind,
1239
 
                    reference_revision=reference_revision,
1240
 
                    symlink_target=symlink_target)
1241
 
        self.flush()
1242
 
 
1243
 
    def _index_add_entry(
1244
 
            self, path, kind, flags=0, reference_revision=None,
1245
 
            symlink_target=None):
 
1241
    def _index_add_entry(self, path, kind, flags=0, reference_revision=None):
1246
1242
        if kind == "directory":
1247
1243
            # Git indexes don't contain directories
1248
1244
            return
1249
 
        elif kind == "file":
 
1245
        if kind == "file":
1250
1246
            blob = Blob()
1251
1247
            try:
1252
1248
                file, stat_val = self.get_file_with_stat(path)
1271
1267
                # old index
1272
1268
                stat_val = os.stat_result(
1273
1269
                    (stat.S_IFLNK, 0, 0, 0, 0, 0, 0, 0, 0, 0))
1274
 
            if symlink_target is None:
1275
 
                symlink_target = self.get_symlink_target(path)
1276
 
            blob.set_raw_string(encode_git_path(symlink_target))
 
1270
            blob.set_raw_string(encode_git_path(self.get_symlink_target(path)))
1277
1271
            # Add object to the repository if it didn't exist yet
1278
1272
            if blob.id not in self.store:
1279
1273
                self.store.add_object(blob)
1307
1301
        if self._versioned_dirs is not None:
1308
1302
            self._ensure_versioned_dir(index_path)
1309
1303
 
1310
 
    def iter_git_objects(self):
1311
 
        for p, entry in self._recurse_index_entries():
1312
 
            yield p, entry.sha, entry.mode
1313
 
 
1314
1304
    def _recurse_index_entries(self, index=None, basepath=b"",
1315
1305
                               recurse_nested=False):
1316
1306
        # Iterate over all index entries
1365
1355
                    key = (posixpath.dirname(path), path)
1366
1356
                    if key not in ret and self.is_versioned(path):
1367
1357
                        ret[key] = self._get_dir_ie(path, self.path2id(key[0]))
1368
 
            return ((path, ie) for ((_, path), ie) in sorted(ret.items()))
 
1358
            return ((path, ie) for ((_, path), ie) in sorted(viewitems(ret)))
1369
1359
 
1370
1360
    def iter_references(self):
1371
1361
        if self.supports_tree_reference():
1380
1370
                                posixpath.basename(path).strip("/"), parent_id)
1381
1371
 
1382
1372
    def _get_file_ie(self, name, path, value, parent_id):
1383
 
        if not isinstance(name, str):
 
1373
        if not isinstance(name, text_type):
1384
1374
            raise TypeError(name)
1385
 
        if not isinstance(path, str):
 
1375
        if not isinstance(path, text_type):
1386
1376
            raise TypeError(path)
1387
1377
        if not isinstance(value, tuple) or len(value) != 10:
1388
1378
            raise TypeError(value)
1397
1387
        elif kind == 'tree-reference':
1398
1388
            ie.reference_revision = self.get_reference_revision(path)
1399
1389
        else:
1400
 
            ie.git_sha1 = sha
1401
 
            ie.text_size = size
 
1390
            try:
 
1391
                data = self.get_file_text(path)
 
1392
            except errors.NoSuchFile:
 
1393
                data = None
 
1394
            except IOError as e:
 
1395
                if e.errno != errno.ENOENT:
 
1396
                    raise
 
1397
                data = None
 
1398
            if data is None:
 
1399
                data = self.branch.repository._git.object_store[sha].data
 
1400
            ie.text_sha1 = osutils.sha_string(data)
 
1401
            ie.text_size = len(data)
1402
1402
            ie.executable = bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
1403
1403
        return ie
1404
1404
 
1565
1565
            self._versioned_dirs = None
1566
1566
            self.flush()
1567
1567
 
 
1568
    def find_related_paths_across_trees(self, paths, trees=[],
 
1569
                                        require_versioned=True):
 
1570
        if paths is None:
 
1571
            return None
 
1572
 
 
1573
        if require_versioned:
 
1574
            trees = [self] + (trees if trees is not None else [])
 
1575
            unversioned = set()
 
1576
            for p in paths:
 
1577
                for t in trees:
 
1578
                    if t.is_versioned(p):
 
1579
                        break
 
1580
                else:
 
1581
                    unversioned.add(p)
 
1582
            if unversioned:
 
1583
                raise errors.PathsNotVersionedError(unversioned)
 
1584
 
 
1585
        return filter(self.is_versioned, paths)
 
1586
 
1568
1587
    def path_content_summary(self, path):
1569
1588
        """See Tree.path_content_summary."""
1570
1589
        try:
1591
1610
            return (kind, None, None, None)
1592
1611
 
1593
1612
    def stored_kind(self, relpath):
1594
 
        if relpath == '':
1595
 
            return 'directory'
1596
1613
        (index, index_path) = self._lookup_index(encode_git_path(relpath))
1597
1614
        if index is None:
1598
 
            return None
 
1615
            return kind
1599
1616
        try:
1600
1617
            mode = index[index_path].mode
1601
1618
        except KeyError:
1602
 
            for p in index:
1603
 
                if osutils.is_inside(
1604
 
                        decode_git_path(index_path), decode_git_path(p)):
1605
 
                    return 'directory'
1606
 
            return None
 
1619
            return kind
1607
1620
        else:
1608
 
            return mode_kind(mode)
 
1621
            if S_ISGITLINK(mode):
 
1622
                return 'tree-reference'
 
1623
            return 'directory'
1609
1624
 
1610
1625
    def kind(self, relpath):
1611
1626
        kind = osutils.file_kind(self.abspath(relpath))
1619
1634
    def _live_entry(self, relpath):
1620
1635
        raise NotImplementedError(self._live_entry)
1621
1636
 
 
1637
    def preview_transform(self, pb=None):
 
1638
        from ..bzr.transform import TransformPreview
 
1639
        return TransformPreview(self, pb=pb)
 
1640
 
1622
1641
    def transform(self, pb=None):
1623
1642
        from .transform import GitTreeTransform
1624
1643
        return GitTreeTransform(self, pb=pb)
1625
1644
 
1626
 
    def has_changes(self, _from_tree=None):
1627
 
        """Quickly check that the tree contains at least one commitable change.
1628
 
 
1629
 
        :param _from_tree: tree to compare against to find changes (default to
1630
 
            the basis tree and is intended to be used by tests).
1631
 
 
1632
 
        :return: True if a change is found. False otherwise
1633
 
        """
1634
 
        with self.lock_read():
1635
 
            # Check pending merges
1636
 
            if len(self.get_parent_ids()) > 1:
1637
 
                return True
1638
 
            if _from_tree is None:
1639
 
                _from_tree = self.basis_tree()
1640
 
            changes = self.iter_changes(_from_tree)
1641
 
            if self.supports_symlinks():
1642
 
                # Fast path for has_changes.
1643
 
                try:
1644
 
                    change = next(changes)
1645
 
                    if change.path[1] == '':
1646
 
                        next(changes)
1647
 
                    return True
1648
 
                except StopIteration:
1649
 
                    # No changes
1650
 
                    return False
1651
 
            else:
1652
 
                # Slow path for has_changes.
1653
 
                # Handle platforms that do not support symlinks in the
1654
 
                # conditional below. This is slower than the try/except
1655
 
                # approach below that but we don't have a choice as we
1656
 
                # need to be sure that all symlinks are removed from the
1657
 
                # entire changeset. This is because in platforms that
1658
 
                # do not support symlinks, they show up as None in the
1659
 
                # working copy as compared to the repository.
1660
 
                # Also, exclude root as mention in the above fast path.
1661
 
                changes = filter(
1662
 
                    lambda c: c[6][0] != 'symlink' and c[4] != (None, None),
1663
 
                    changes)
1664
 
                try:
1665
 
                    next(iter(changes))
1666
 
                except StopIteration:
1667
 
                    return False
1668
 
                return True
 
1645
 
 
1646
class InterToIndexGitTree(InterGitTrees):
 
1647
    """InterTree that works between a Git revision tree and an index."""
 
1648
 
 
1649
    def __init__(self, source, target):
 
1650
        super(InterToIndexGitTree, self).__init__(source, target)
 
1651
        if self.source.store == self.target.store:
 
1652
            self.store = self.source.store
 
1653
        else:
 
1654
            self.store = OverlayObjectStore(
 
1655
                [self.source.store, self.target.store])
 
1656
        self.rename_detector = RenameDetector(self.store)
 
1657
 
 
1658
    @classmethod
 
1659
    def is_compatible(cls, source, target):
 
1660
        return (isinstance(source, GitRevisionTree) and
 
1661
                isinstance(target, MutableGitIndexTree))
 
1662
 
 
1663
    def _iter_git_changes(self, want_unchanged=False, specific_files=None,
 
1664
                          require_versioned=False, extra_trees=None,
 
1665
                          want_unversioned=False, include_trees=True):
 
1666
        trees = [self.source]
 
1667
        if extra_trees is not None:
 
1668
            trees.extend(extra_trees)
 
1669
        if specific_files is not None:
 
1670
            specific_files = self.target.find_related_paths_across_trees(
 
1671
                specific_files, trees,
 
1672
                require_versioned=require_versioned)
 
1673
        # TODO(jelmer): Restrict to specific_files, for performance reasons.
 
1674
        with self.lock_read():
 
1675
            changes, target_extras = changes_between_git_tree_and_working_copy(
 
1676
                self.source.store, self.source.tree,
 
1677
                self.target, want_unchanged=want_unchanged,
 
1678
                want_unversioned=want_unversioned,
 
1679
                rename_detector=self.rename_detector,
 
1680
                include_trees=include_trees)
 
1681
            return changes, set(), target_extras
 
1682
 
 
1683
 
 
1684
_mod_tree.InterTree.register_optimiser(InterToIndexGitTree)
 
1685
 
 
1686
 
 
1687
class InterFromIndexGitTree(InterGitTrees):
 
1688
    """InterTree that works between a Git revision tree and an index."""
 
1689
 
 
1690
    def __init__(self, source, target):
 
1691
        super(InterFromIndexGitTree, self).__init__(source, target)
 
1692
        if self.source.store == self.target.store:
 
1693
            self.store = self.source.store
 
1694
        else:
 
1695
            self.store = OverlayObjectStore(
 
1696
                [self.source.store, self.target.store])
 
1697
        self.rename_detector = RenameDetector(self.store)
 
1698
 
 
1699
    @classmethod
 
1700
    def is_compatible(cls, source, target):
 
1701
        return (isinstance(target, GitRevisionTree) and
 
1702
                isinstance(source, MutableGitIndexTree))
 
1703
 
 
1704
    def _iter_git_changes(self, want_unchanged=False, specific_files=None,
 
1705
                          require_versioned=False, extra_trees=None,
 
1706
                          want_unversioned=False, include_trees=True):
 
1707
        trees = [self.source]
 
1708
        if extra_trees is not None:
 
1709
            trees.extend(extra_trees)
 
1710
        if specific_files is not None:
 
1711
            specific_files = self.target.find_related_paths_across_trees(
 
1712
                specific_files, trees,
 
1713
                require_versioned=require_versioned)
 
1714
        # TODO(jelmer): Restrict to specific_files, for performance reasons.
 
1715
        with self.lock_read():
 
1716
            from_tree_sha, extras = snapshot_workingtree(self.source, want_unversioned=want_unversioned)
 
1717
            return tree_changes(
 
1718
                self.store, from_tree_sha, self.target.tree,
 
1719
                include_trees=include_trees,
 
1720
                rename_detector=self.rename_detector,
 
1721
                want_unchanged=want_unchanged, change_type_same=True), extras
 
1722
 
 
1723
 
 
1724
_mod_tree.InterTree.register_optimiser(InterFromIndexGitTree)
 
1725
 
 
1726
 
 
1727
class InterIndexGitTree(InterGitTrees):
 
1728
    """InterTree that works between a Git revision tree and an index."""
 
1729
 
 
1730
    def __init__(self, source, target):
 
1731
        super(InterIndexGitTree, self).__init__(source, target)
 
1732
        if self.source.store == self.target.store:
 
1733
            self.store = self.source.store
 
1734
        else:
 
1735
            self.store = OverlayObjectStore(
 
1736
                [self.source.store, self.target.store])
 
1737
        self.rename_detector = RenameDetector(self.store)
 
1738
 
 
1739
    @classmethod
 
1740
    def is_compatible(cls, source, target):
 
1741
        return (isinstance(target, MutableGitIndexTree) and
 
1742
                isinstance(source, MutableGitIndexTree))
 
1743
 
 
1744
    def _iter_git_changes(self, want_unchanged=False, specific_files=None,
 
1745
                          require_versioned=False, extra_trees=None,
 
1746
                          want_unversioned=False, include_trees=True):
 
1747
        trees = [self.source]
 
1748
        if extra_trees is not None:
 
1749
            trees.extend(extra_trees)
 
1750
        if specific_files is not None:
 
1751
            specific_files = self.target.find_related_paths_across_trees(
 
1752
                specific_files, trees,
 
1753
                require_versioned=require_versioned)
 
1754
        # TODO(jelmer): Restrict to specific_files, for performance reasons.
 
1755
        with self.lock_read():
 
1756
            from_tree_sha, from_extras = snapshot_workingtree(
 
1757
                self.source, want_unversioned=want_unversioned)
 
1758
            to_tree_sha, to_extras = snapshot_workingtree(
 
1759
                self.target, want_unversioned=want_unversioned)
 
1760
            changes = tree_changes(
 
1761
                self.store, from_tree_sha, to_tree_sha,
 
1762
                include_trees=include_trees,
 
1763
                rename_detector=self.rename_detector,
 
1764
                want_unchanged=want_unchanged, change_type_same=True)
 
1765
            return changes, from_extras, to_extras
 
1766
 
 
1767
 
 
1768
_mod_tree.InterTree.register_optimiser(InterIndexGitTree)
1669
1769
 
1670
1770
 
1671
1771
def snapshot_workingtree(target, want_unversioned=False):
1715
1815
                        target.store.add_object(blob)
1716
1816
                blobs[path] = (live_entry.sha, cleanup_mode(live_entry.mode))
1717
1817
    if want_unversioned:
1718
 
        for extra in target._iter_files_recursive(include_dirs=False):
 
1818
        for e in target._iter_files_recursive(include_dirs=False):
1719
1819
            try:
1720
 
                extra, accessible = osutils.normalized_filename(extra)
 
1820
                e, accessible = osutils.normalized_filename(e)
1721
1821
            except UnicodeDecodeError:
1722
1822
                raise errors.BadFilenameEncoding(
1723
 
                    extra, osutils._fs_enc)
1724
 
            np = encode_git_path(extra)
 
1823
                    e, osutils._fs_enc)
 
1824
            np = encode_git_path(e)
1725
1825
            if np in blobs:
1726
1826
                continue
1727
 
            st = target._lstat(extra)
 
1827
            st = target._lstat(e)
1728
1828
            if stat.S_ISDIR(st.st_mode):
1729
1829
                blob = Tree()
1730
1830
            elif stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode):
1731
1831
                blob = blob_from_path_and_stat(
1732
 
                    target.abspath(extra).encode(osutils._fs_enc), st)
 
1832
                    target.abspath(e).encode(osutils._fs_enc), st)
1733
1833
            else:
1734
1834
                continue
1735
1835
            target.store.add_object(blob)
1737
1837
            extras.add(np)
1738
1838
    return commit_tree(
1739
1839
        target.store, dirified + [(p, s, m) for (p, (s, m)) in blobs.items()]), extras
 
1840
 
 
1841
 
 
1842
def changes_between_git_tree_and_working_copy(source_store, from_tree_sha, target,
 
1843
                                              want_unchanged=False,
 
1844
                                              want_unversioned=False,
 
1845
                                              rename_detector=None,
 
1846
                                              include_trees=True):
 
1847
    """Determine the changes between a git tree and a working tree with index.
 
1848
 
 
1849
    """
 
1850
    to_tree_sha, extras = snapshot_workingtree(target, want_unversioned=want_unversioned)
 
1851
    store = OverlayObjectStore([source_store, target.store])
 
1852
    return tree_changes(
 
1853
        store, from_tree_sha, to_tree_sha, include_trees=include_trees,
 
1854
        rename_detector=rename_detector,
 
1855
        want_unchanged=want_unchanged, change_type_same=True), extras