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

  • Committer: Jelmer Vernooij
  • Date: 2018-05-19 13:16:11 UTC
  • mto: (6968.4.3 git-archive)
  • mto: This revision was merged to the branch mainline in revision 6972.
  • Revision ID: jelmer@jelmer.uk-20180519131611-l9h9ud41j7qg1m03
Move tar/zip to breezy.archive.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2009 Jelmer Vernooij <jelmer@samba.org>
 
1
# Copyright (C) 2009-2018 Jelmer Vernooij <jelmer@jelmer.uk>
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
 
18
18
"""Git Trees."""
19
19
 
20
 
from bzrlib import (
 
20
from __future__ import absolute_import
 
21
 
 
22
import errno
 
23
from io import BytesIO
 
24
import os
 
25
 
 
26
from dulwich.index import (
 
27
    index_entry_from_stat,
 
28
    )
 
29
from dulwich.object_store import (
 
30
    tree_lookup_path,
 
31
    OverlayObjectStore,
 
32
    )
 
33
from dulwich.objects import (
 
34
    Blob,
 
35
    Tree,
 
36
    ZERO_SHA,
 
37
    S_IFGITLINK,
 
38
    S_ISGITLINK,
 
39
    )
 
40
import stat
 
41
import posixpath
 
42
 
 
43
from ... import (
 
44
    controldir as _mod_controldir,
21
45
    delta,
22
46
    errors,
 
47
    lock,
 
48
    mutabletree,
 
49
    osutils,
23
50
    revisiontree,
24
 
    tree,
 
51
    trace,
 
52
    tree as _mod_tree,
 
53
    workingtree,
25
54
    )
 
55
from ...revision import NULL_REVISION
26
56
 
27
 
from bzrlib.plugins.git.inventory import (
28
 
    GitInventory,
29
 
    )
30
 
from bzrlib.plugins.git.mapping import (
 
57
from .mapping import (
31
58
    mode_is_executable,
32
59
    mode_kind,
 
60
    GitFileIdMap,
 
61
    default_mapping,
33
62
    )
34
63
 
35
64
 
 
65
class GitTreeDirectory(_mod_tree.TreeDirectory):
 
66
 
 
67
    __slots__ = ['file_id', 'name', 'parent_id', 'children']
 
68
 
 
69
    def __init__(self, file_id, name, parent_id):
 
70
        self.file_id = file_id
 
71
        self.name = name
 
72
        self.parent_id = parent_id
 
73
        # TODO(jelmer)
 
74
        self.children = {}
 
75
 
 
76
    @property
 
77
    def kind(self):
 
78
        return 'directory'
 
79
 
 
80
    @property
 
81
    def executable(self):
 
82
        return False
 
83
 
 
84
    def copy(self):
 
85
        return self.__class__(
 
86
            self.file_id, self.name, self.parent_id)
 
87
 
 
88
    def __repr__(self):
 
89
        return "%s(file_id=%r, name=%r, parent_id=%r)" % (
 
90
            self.__class__.__name__, self.file_id, self.name,
 
91
            self.parent_id)
 
92
 
 
93
    def __eq__(self, other):
 
94
        return (self.kind == other.kind and
 
95
                self.file_id == other.file_id and
 
96
                self.name == other.name and
 
97
                self.parent_id == other.parent_id)
 
98
 
 
99
 
 
100
class GitTreeFile(_mod_tree.TreeFile):
 
101
 
 
102
    __slots__ = ['file_id', 'name', 'parent_id', 'text_size', 'text_sha1',
 
103
                 'executable']
 
104
 
 
105
    def __init__(self, file_id, name, parent_id, text_size=None,
 
106
                 text_sha1=None, executable=None):
 
107
        self.file_id = file_id
 
108
        self.name = name
 
109
        self.parent_id = parent_id
 
110
        self.text_size = text_size
 
111
        self.text_sha1 = text_sha1
 
112
        self.executable = executable
 
113
 
 
114
    @property
 
115
    def kind(self):
 
116
        return 'file'
 
117
 
 
118
    def __eq__(self, other):
 
119
        return (self.kind == other.kind and
 
120
                self.file_id == other.file_id and
 
121
                self.name == other.name and
 
122
                self.parent_id == other.parent_id and
 
123
                self.text_sha1 == other.text_sha1 and
 
124
                self.text_size == other.text_size and
 
125
                self.executable == other.executable)
 
126
 
 
127
    def __repr__(self):
 
128
        return "%s(file_id=%r, name=%r, parent_id=%r, text_size=%r, text_sha1=%r, executable=%r)" % (
 
129
            type(self).__name__, self.file_id, self.name, self.parent_id,
 
130
            self.text_size, self.text_sha1, self.executable)
 
131
 
 
132
    def copy(self):
 
133
        ret = self.__class__(
 
134
                self.file_id, self.name, self.parent_id)
 
135
        ret.text_sha1 = self.text_sha1
 
136
        ret.text_size = self.text_size
 
137
        ret.executable = self.executable
 
138
        return ret
 
139
 
 
140
 
 
141
class GitTreeSymlink(_mod_tree.TreeLink):
 
142
 
 
143
    __slots__ = ['file_id', 'name', 'parent_id', 'symlink_target']
 
144
 
 
145
    def __init__(self, file_id, name, parent_id,
 
146
                 symlink_target=None):
 
147
        self.file_id = file_id
 
148
        self.name = name
 
149
        self.parent_id = parent_id
 
150
        self.symlink_target = symlink_target
 
151
 
 
152
    @property
 
153
    def kind(self):
 
154
        return 'symlink'
 
155
 
 
156
    @property
 
157
    def executable(self):
 
158
        return False
 
159
 
 
160
    @property
 
161
    def text_size(self):
 
162
        return None
 
163
 
 
164
    def __repr__(self):
 
165
        return "%s(file_id=%r, name=%r, parent_id=%r, symlink_target=%r)" % (
 
166
            type(self).__name__, self.file_id, self.name, self.parent_id,
 
167
            self.symlink_target)
 
168
 
 
169
    def __eq__(self, other):
 
170
        return (self.kind == other.kind and
 
171
                self.file_id == other.file_id and
 
172
                self.name == other.name and
 
173
                self.parent_id == other.parent_id and
 
174
                self.symlink_target == other.symlink_target)
 
175
 
 
176
    def copy(self):
 
177
        return self.__class__(
 
178
                self.file_id, self.name, self.parent_id,
 
179
                self.symlink_target)
 
180
 
 
181
 
 
182
class GitTreeSubmodule(_mod_tree.TreeLink):
 
183
 
 
184
    __slots__ = ['file_id', 'name', 'parent_id', 'reference_revision']
 
185
 
 
186
    def __init__(self, file_id, name, parent_id, reference_revision=None):
 
187
        self.file_id = file_id
 
188
        self.name = name
 
189
        self.parent_id = parent_id
 
190
        self.reference_revision = reference_revision
 
191
 
 
192
    @property
 
193
    def kind(self):
 
194
        return 'tree-reference'
 
195
 
 
196
    def __repr__(self):
 
197
        return "%s(file_id=%r, name=%r, parent_id=%r, reference_revision=%r)" % (
 
198
            type(self).__name__, self.file_id, self.name, self.parent_id,
 
199
            self.reference_revision)
 
200
 
 
201
    def __eq__(self, other):
 
202
        return (self.kind == other.kind and
 
203
                self.file_id == other.file_id and
 
204
                self.name == other.name and
 
205
                self.parent_id == other.parent_id and
 
206
                self.reference_revision == other.reference_revision)
 
207
 
 
208
    def copy(self):
 
209
        return self.__class__(
 
210
                self.file_id, self.name, self.parent_id,
 
211
                self.reference_revision)
 
212
 
 
213
 
 
214
entry_factory = {
 
215
    'directory': GitTreeDirectory,
 
216
    'file': GitTreeFile,
 
217
    'symlink': GitTreeSymlink,
 
218
    'tree-reference': GitTreeSubmodule,
 
219
    }
 
220
 
 
221
 
 
222
def ensure_normalized_path(path):
 
223
    """Check whether path is normalized.
 
224
 
 
225
    :raises InvalidNormalization: When path is not normalized, and cannot be
 
226
        accessed on this platform by the normalized path.
 
227
    :return: The NFC normalised version of path.
 
228
    """
 
229
    norm_path, can_access = osutils.normalized_filename(path)
 
230
    if norm_path != path:
 
231
        if can_access:
 
232
            return norm_path
 
233
        else:
 
234
            raise errors.InvalidNormalization(path)
 
235
    return path
 
236
 
 
237
 
36
238
class GitRevisionTree(revisiontree.RevisionTree):
37
239
    """Revision tree implementation based on Git objects."""
38
240
 
39
241
    def __init__(self, repository, revision_id):
40
242
        self._revision_id = revision_id
41
243
        self._repository = repository
42
 
        store = repository._git.object_store
43
 
        assert isinstance(revision_id, str)
44
 
        git_id, self.mapping = repository.lookup_bzr_revision_id(revision_id)
45
 
        try:
46
 
            commit = store[git_id]
47
 
        except KeyError, r:
48
 
            raise errors.NoSuchRevision(repository, revision_id)
49
 
        self.tree = commit.tree
50
 
        fileid_map = self.mapping.get_fileid_map(store.__getitem__, self.tree)
51
 
        self._inventory = GitInventory(self.tree, self.mapping, fileid_map,
52
 
            store, revision_id)
 
244
        self.store = repository._git.object_store
 
245
        if not isinstance(revision_id, bytes):
 
246
            raise TypeError(revision_id)
 
247
        self.commit_id, self.mapping = repository.lookup_bzr_revision_id(revision_id)
 
248
        if revision_id == NULL_REVISION:
 
249
            self.tree = None
 
250
            self.mapping = default_mapping
 
251
            self._fileid_map = GitFileIdMap(
 
252
                {},
 
253
                default_mapping)
 
254
        else:
 
255
            try:
 
256
                commit = self.store[self.commit_id]
 
257
            except KeyError:
 
258
                raise errors.NoSuchRevision(repository, revision_id)
 
259
            self.tree = commit.tree
 
260
            self._fileid_map = self.mapping.get_fileid_map(self.store.__getitem__, self.tree)
 
261
 
 
262
    def _get_nested_repository(self, path):
 
263
        nested_repo_transport = self._repository.user_transport.clone(path)
 
264
        nested_controldir = _mod_controldir.ControlDir.open_from_transport(nested_repo_transport)
 
265
        return nested_controldir.find_repository()
 
266
 
 
267
    def supports_rename_tracking(self):
 
268
        return False
 
269
 
 
270
    def get_file_revision(self, path, file_id=None):
 
271
        change_scanner = self._repository._file_change_scanner
 
272
        if self.commit_id == ZERO_SHA:
 
273
            return NULL_REVISION
 
274
        (path, commit_id) = change_scanner.find_last_change_revision(
 
275
            path.encode('utf-8'), self.commit_id)
 
276
        return self._repository.lookup_foreign_revision_id(commit_id, self.mapping)
 
277
 
 
278
    def get_file_mtime(self, path, file_id=None):
 
279
        try:
 
280
            revid = self.get_file_revision(path, file_id)
 
281
        except KeyError:
 
282
            raise _mod_tree.FileTimestampUnavailable(path)
 
283
        try:
 
284
            rev = self._repository.get_revision(revid)
 
285
        except errors.NoSuchRevision:
 
286
            raise _mod_tree.FileTimestampUnavailable(path)
 
287
        return rev.timestamp
 
288
 
 
289
    def id2path(self, file_id):
 
290
        try:
 
291
            path = self._fileid_map.lookup_path(file_id)
 
292
        except ValueError:
 
293
            raise errors.NoSuchId(self, file_id)
 
294
        path = path.decode('utf-8')
 
295
        if self.is_versioned(path):
 
296
            return path
 
297
        raise errors.NoSuchId(self, file_id)
 
298
 
 
299
    def is_versioned(self, path):
 
300
        return self.has_filename(path)
 
301
 
 
302
    def path2id(self, path):
 
303
        if self.mapping.is_special_file(path):
 
304
            return None
 
305
        return self._fileid_map.lookup_file_id(path.encode('utf-8'))
 
306
 
 
307
    def all_file_ids(self):
 
308
        return set(self._fileid_map.all_file_ids())
 
309
 
 
310
    def all_versioned_paths(self):
 
311
        ret = set()
 
312
        todo = set([(store, '', self.tree)])
 
313
        while todo:
 
314
            (store, path, tree_id) = todo.pop()
 
315
            if tree_id is None:
 
316
                continue
 
317
            tree = store[tree_id]
 
318
            for name, mode, hexsha in tree.items():
 
319
                subpath = posixpath.join(path, name)
 
320
                if stat.S_ISDIR(mode):
 
321
                    todo.add((store, subpath, hexsha))
 
322
                else:
 
323
                    ret.add(subpath)
 
324
        return ret
 
325
 
 
326
    def get_root_id(self):
 
327
        if self.tree is None:
 
328
            return None
 
329
        return self.path2id("")
 
330
 
 
331
    def has_or_had_id(self, file_id):
 
332
        try:
 
333
            path = self.id2path(file_id)
 
334
        except errors.NoSuchId:
 
335
            return False
 
336
        return True
 
337
 
 
338
    def has_id(self, file_id):
 
339
        try:
 
340
            path = self.id2path(file_id)
 
341
        except errors.NoSuchId:
 
342
            return False
 
343
        return self.has_filename(path)
 
344
 
 
345
    def _lookup_path(self, path):
 
346
        if self.tree is None:
 
347
            raise errors.NoSuchFile(path)
 
348
        try:
 
349
            (mode, hexsha) = tree_lookup_path(self.store.__getitem__, self.tree,
 
350
                path.encode('utf-8'))
 
351
        except KeyError:
 
352
            raise errors.NoSuchFile(self, path)
 
353
        else:
 
354
            return (self.store, mode, hexsha)
 
355
 
 
356
    def is_executable(self, path, file_id=None):
 
357
        (store, mode, hexsha) = self._lookup_path(path)
 
358
        if mode is None:
 
359
            # the tree root is a directory
 
360
            return False
 
361
        return mode_is_executable(mode)
 
362
 
 
363
    def kind(self, path, file_id=None):
 
364
        (store, mode, hexsha) = self._lookup_path(path)
 
365
        if mode is None:
 
366
            # the tree root is a directory
 
367
            return "directory"
 
368
        return mode_kind(mode)
 
369
 
 
370
    def has_filename(self, path):
 
371
        try:
 
372
            self._lookup_path(path)
 
373
        except errors.NoSuchFile:
 
374
            return False
 
375
        else:
 
376
            return True
 
377
 
 
378
    def list_files(self, include_root=False, from_dir=None, recursive=True):
 
379
        if self.tree is None:
 
380
            return
 
381
        if from_dir is None:
 
382
            from_dir = u""
 
383
        (store, mode, hexsha) = self._lookup_path(from_dir)
 
384
        if mode is None: # Root
 
385
            root_ie = self._get_dir_ie(b"", None)
 
386
        else:
 
387
            parent_path = posixpath.dirname(from_dir.encode("utf-8"))
 
388
            parent_id = self._fileid_map.lookup_file_id(parent_path)
 
389
            if mode_kind(mode) == 'directory':
 
390
                root_ie = self._get_dir_ie(from_dir.encode("utf-8"), parent_id)
 
391
            else:
 
392
                root_ie = self._get_file_ie(store, from_dir.encode("utf-8"),
 
393
                    posixpath.basename(from_dir), mode, hexsha)
 
394
        if from_dir != "" or include_root:
 
395
            yield (from_dir, "V", root_ie.kind, root_ie.file_id, root_ie)
 
396
        todo = set()
 
397
        if root_ie.kind == 'directory':
 
398
            todo.add((store, from_dir.encode("utf-8"), hexsha, root_ie.file_id))
 
399
        while todo:
 
400
            (store, path, hexsha, parent_id) = todo.pop()
 
401
            tree = store[hexsha]
 
402
            for name, mode, hexsha in tree.iteritems():
 
403
                if self.mapping.is_special_file(name):
 
404
                    continue
 
405
                child_path = posixpath.join(path, name)
 
406
                if stat.S_ISDIR(mode):
 
407
                    ie = self._get_dir_ie(child_path, parent_id)
 
408
                    if recursive:
 
409
                        todo.add((store, child_path, hexsha, ie.file_id))
 
410
                else:
 
411
                    ie = self._get_file_ie(store, child_path, name, mode, hexsha, parent_id)
 
412
                yield child_path.decode('utf-8'), "V", ie.kind, ie.file_id, ie
 
413
 
 
414
    def _get_file_ie(self, store, path, name, mode, hexsha, parent_id):
 
415
        if type(path) is not bytes:
 
416
            raise TypeError(path)
 
417
        if type(name) is not bytes:
 
418
            raise TypeError(name)
 
419
        kind = mode_kind(mode)
 
420
        file_id = self._fileid_map.lookup_file_id(path)
 
421
        ie = entry_factory[kind](file_id, name.decode("utf-8"), parent_id)
 
422
        if kind == 'symlink':
 
423
            ie.symlink_target = store[hexsha].data.decode('utf-8')
 
424
        elif kind == 'tree-reference':
 
425
            ie.reference_revision = self.mapping.revision_id_foreign_to_bzr(hexsha)
 
426
        else:
 
427
            data = store[hexsha].data
 
428
            ie.text_sha1 = osutils.sha_string(data)
 
429
            ie.text_size = len(data)
 
430
            ie.executable = mode_is_executable(mode)
 
431
        return ie
 
432
 
 
433
    def _get_dir_ie(self, path, parent_id):
 
434
        file_id = self._fileid_map.lookup_file_id(path)
 
435
        return GitTreeDirectory(file_id,
 
436
            posixpath.basename(path).decode("utf-8"), parent_id)
 
437
 
 
438
    def iter_child_entries(self, path, file_id=None):
 
439
        (store, mode, tree_sha) = self._lookup_path(path)
 
440
 
 
441
        if not stat.S_ISDIR(mode):
 
442
            return
 
443
 
 
444
        encoded_path = path.encode('utf-8')
 
445
        file_id = self.path2id(path)
 
446
        tree = store[tree_sha]
 
447
        for name, mode, hexsha in tree.iteritems():
 
448
            if self.mapping.is_special_file(name):
 
449
                continue
 
450
            child_path = posixpath.join(encoded_path, name)
 
451
            if stat.S_ISDIR(mode):
 
452
                yield self._get_dir_ie(child_path, file_id)
 
453
            else:
 
454
                yield self._get_file_ie(store, child_path, name, mode, hexsha,
 
455
                                        file_id)
 
456
 
 
457
    def iter_entries_by_dir(self, specific_files=None, yield_parents=False):
 
458
        if self.tree is None:
 
459
            return
 
460
        if yield_parents:
 
461
            # TODO(jelmer): Support yield parents
 
462
            raise NotImplementedError
 
463
        if specific_files is not None:
 
464
            if specific_files in ([""], []):
 
465
                specific_files = None
 
466
            else:
 
467
                specific_files = set([p.encode('utf-8') for p in specific_files])
 
468
        todo = set([(self.store, "", self.tree, None)])
 
469
        while todo:
 
470
            store, path, tree_sha, parent_id = todo.pop()
 
471
            ie = self._get_dir_ie(path, parent_id)
 
472
            if specific_files is None or path in specific_files:
 
473
                yield path.decode("utf-8"), ie
 
474
            tree = store[tree_sha]
 
475
            for name, mode, hexsha in tree.iteritems():
 
476
                if self.mapping.is_special_file(name):
 
477
                    continue
 
478
                child_path = posixpath.join(path, name)
 
479
                if stat.S_ISDIR(mode):
 
480
                    if (specific_files is None or
 
481
                        any(filter(lambda p: p.startswith(child_path), specific_files))):
 
482
                        todo.add((store, child_path, hexsha, ie.file_id))
 
483
                elif specific_files is None or child_path in specific_files:
 
484
                    yield (child_path.decode("utf-8"),
 
485
                            self._get_file_ie(store, child_path, name, mode, hexsha,
 
486
                           ie.file_id))
53
487
 
54
488
    def get_revision_id(self):
55
489
        """See RevisionTree.get_revision_id."""
56
490
        return self._revision_id
57
491
 
58
 
    def get_file_text(self, file_id, path=None):
 
492
    def get_file_sha1(self, path, file_id=None, stat_value=None):
 
493
        if self.tree is None:
 
494
            raise errors.NoSuchFile(path)
 
495
        return osutils.sha_string(self.get_file_text(path, file_id))
 
496
 
 
497
    def get_file_verifier(self, path, file_id=None, stat_value=None):
 
498
        (store, mode, hexsha) = self._lookup_path(path)
 
499
        return ("GIT", hexsha)
 
500
 
 
501
    def get_file_text(self, path, file_id=None):
59
502
        """See RevisionTree.get_file_text."""
60
 
        if path is not None:
61
 
            entry = self._inventory._get_ie(path)
62
 
        else:
63
 
            entry = self._inventory[file_id]
64
 
        if entry.kind in ('directory', 'tree-reference'):
65
 
            return ""
66
 
        return entry.object.data
 
503
        (store, mode, hexsha) = self._lookup_path(path)
 
504
        if stat.S_ISREG(mode):
 
505
            return store[hexsha].data
 
506
        else:
 
507
            return b""
 
508
 
 
509
    def get_symlink_target(self, path, file_id=None):
 
510
        """See RevisionTree.get_symlink_target."""
 
511
        (store, mode, hexsha) = self._lookup_path(path)
 
512
        if stat.S_ISLNK(mode):
 
513
            return store[hexsha].data.decode('utf-8')
 
514
        else:
 
515
            return None
 
516
 
 
517
    def get_reference_revision(self, path, file_id=None):
 
518
        """See RevisionTree.get_symlink_target."""
 
519
        (store, mode, hexsha) = self._lookup_path(path)
 
520
        if S_ISGITLINK(mode):
 
521
            nested_repo = self._get_nested_repository(path)
 
522
            return nested_repo.lookup_foreign_revision_id(hexsha)
 
523
        else:
 
524
            return None
 
525
 
 
526
    def _comparison_data(self, entry, path):
 
527
        if entry is None:
 
528
            return None, False, None
 
529
        return entry.kind, entry.executable, None
 
530
 
 
531
    def path_content_summary(self, path):
 
532
        """See Tree.path_content_summary."""
 
533
        try:
 
534
            (store, mode, hexsha) = self._lookup_path(path)
 
535
        except errors.NoSuchFile:
 
536
            return ('missing', None, None, None)
 
537
        kind = mode_kind(mode)
 
538
        if kind == 'file':
 
539
            executable = mode_is_executable(mode)
 
540
            contents = store[hexsha].data
 
541
            return (kind, len(contents), executable, osutils.sha_string(contents))
 
542
        elif kind == 'symlink':
 
543
            return (kind, None, None, store[hexsha].data)
 
544
        elif kind == 'tree-reference':
 
545
            nested_repo = self._get_nested_repository(path)
 
546
            return (kind, None, None,
 
547
                    nested_repo.lookup_foreign_revision_id(hexsha))
 
548
        else:
 
549
            return (kind, None, None, None)
 
550
 
 
551
    def find_related_paths_across_trees(self, paths, trees=[],
 
552
            require_versioned=True):
 
553
        if paths is None:
 
554
            return None
 
555
        if require_versioned:
 
556
            trees = [self] + (trees if trees is not None else [])
 
557
            unversioned = set()
 
558
            for p in paths:
 
559
                for t in trees:
 
560
                    if t.is_versioned(p):
 
561
                        break
 
562
                else:
 
563
                    unversioned.add(p)
 
564
            if unversioned:
 
565
                raise errors.PathsNotVersionedError(unversioned)
 
566
        return filter(self.is_versioned, paths)
 
567
 
 
568
    def _iter_tree_contents(self, include_trees=False):
 
569
        if self.tree is None:
 
570
            return iter([])
 
571
        return self.store.iter_tree_contents(
 
572
                self.tree, include_trees=include_trees)
67
573
 
68
574
 
69
575
def tree_delta_from_git_changes(changes, mapping,
70
 
        (old_fileid_map, new_fileid_map), specific_file=None,
71
 
        require_versioned=False):
 
576
        fileid_maps, specific_files=None,
 
577
        require_versioned=False, include_root=False,
 
578
        target_extras=None):
72
579
    """Create a TreeDelta from two git trees.
73
580
 
74
581
    source and target are iterators over tuples with:
75
582
        (filename, sha, mode)
76
583
    """
 
584
    (old_fileid_map, new_fileid_map) = fileid_maps
 
585
    if target_extras is None:
 
586
        target_extras = set()
77
587
    ret = delta.TreeDelta()
78
588
    for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
79
 
        if mapping.is_control_file(oldpath):
 
589
        if newpath == u'' and not include_root:
 
590
            continue
 
591
        if not (specific_files is None or
 
592
                (oldpath is not None and osutils.is_inside_or_parent_of_any(specific_files, oldpath)) or
 
593
                (newpath is not None and osutils.is_inside_or_parent_of_any(specific_files, newpath))):
 
594
            continue
 
595
        if mapping.is_special_file(oldpath):
80
596
            oldpath = None
81
 
        if mapping.is_control_file(newpath):
 
597
        if mapping.is_special_file(newpath):
82
598
            newpath = None
83
599
        if oldpath is None and newpath is None:
84
600
            continue
85
601
        if oldpath is None:
86
 
            ret.added.append((newpath, new_fileid_map.lookup_file_id(newpath.encode("utf-8")), mode_kind(newmode)))
 
602
            if newpath in target_extras:
 
603
                ret.unversioned.append(
 
604
                    (osutils.normalized_filename(newpath)[0], None, mode_kind(newmode)))
 
605
            else:
 
606
                file_id = new_fileid_map.lookup_file_id(newpath)
 
607
                ret.added.append((newpath.decode('utf-8'), file_id, mode_kind(newmode)))
87
608
        elif newpath is None:
88
 
            ret.removed.append((oldpath, old_fileid_map.lookup_file_id(oldpath.encode("utf-8")), mode_kind(oldmode)))
 
609
            file_id = old_fileid_map.lookup_file_id(oldpath)
 
610
            ret.removed.append((oldpath.decode('utf-8'), file_id, mode_kind(oldmode)))
89
611
        elif oldpath != newpath:
90
 
            ret.renamed.append((oldpath, newpath, old_fileid_map.lookup_file_id(oldpath.encode("utf-8")), mode_kind(newmode), (oldsha != newsha), (oldmode != newmode)))
 
612
            file_id = old_fileid_map.lookup_file_id(oldpath)
 
613
            ret.renamed.append(
 
614
                (oldpath.decode('utf-8'), newpath.decode('utf-8'), file_id,
 
615
                mode_kind(newmode), (oldsha != newsha),
 
616
                (oldmode != newmode)))
91
617
        elif mode_kind(oldmode) != mode_kind(newmode):
92
 
            ret.kind_changed.append((newpath, new_fileid_map.lookup_file_id(newpath.encode("utf-8")), mode_kind(oldmode), mode_kind(newmode)))
 
618
            file_id = new_fileid_map.lookup_file_id(newpath)
 
619
            ret.kind_changed.append(
 
620
                (newpath.decode('utf-8'), file_id, mode_kind(oldmode),
 
621
                mode_kind(newmode)))
93
622
        elif oldsha != newsha or oldmode != newmode:
94
 
            ret.modified.append((newpath, new_fileid_map.lookup_file_id(newpath.encode("utf-8")), mode_kind(newmode), (oldsha != newsha), (oldmode != newmode)))
 
623
            if stat.S_ISDIR(oldmode) and stat.S_ISDIR(newmode):
 
624
                continue
 
625
            file_id = new_fileid_map.lookup_file_id(newpath)
 
626
            ret.modified.append(
 
627
                (newpath.decode('utf-8'), file_id, mode_kind(newmode),
 
628
                (oldsha != newsha), (oldmode != newmode)))
95
629
        else:
96
 
            ret.unchanged.append((newpath, new_fileid_map.lookup_file_id(newpath.encode("utf-8")), mode_kind(newmode)))
 
630
            file_id = new_fileid_map.lookup_file_id(newpath)
 
631
            ret.unchanged.append((newpath.decode('utf-8'), file_id, mode_kind(newmode)))
 
632
 
97
633
    return ret
98
634
 
99
635
 
100
 
def changes_from_git_changes(changes, mapping, specific_file=None,
101
 
                                require_versioned=False):
 
636
def changes_from_git_changes(changes, mapping, specific_files=None, include_unchanged=False,
 
637
                             target_extras=None):
102
638
    """Create a iter_changes-like generator from a git stream.
103
639
 
104
640
    source and target are iterators over tuples with:
105
641
        (filename, sha, mode)
106
642
    """
 
643
    if target_extras is None:
 
644
        target_extras = set()
107
645
    for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
 
646
        if not (specific_files is None or
 
647
                (oldpath is not None and osutils.is_inside_or_parent_of_any(specific_files, oldpath)) or
 
648
                (newpath is not None and osutils.is_inside_or_parent_of_any(specific_files, newpath))):
 
649
            continue
108
650
        path = (oldpath, newpath)
 
651
        if oldpath is not None and mapping.is_special_file(oldpath):
 
652
            continue
 
653
        if newpath is not None and mapping.is_special_file(newpath):
 
654
            continue
109
655
        if oldpath is None:
110
656
            fileid = mapping.generate_file_id(newpath)
111
657
            oldexe = None
112
658
            oldkind = None
113
659
            oldname = None
114
660
            oldparent = None
 
661
            oldversioned = False
115
662
        else:
116
 
            oldexe = mode_is_executable(oldmode)
117
 
            oldkind = mode_kind(oldmode)
118
 
            try:
119
 
                (oldparentpath, oldname) = oldpath.rsplit("/", 1)
120
 
            except ValueError:
 
663
            oldversioned = True
 
664
            oldpath = oldpath.decode("utf-8")
 
665
            if oldmode:
 
666
                oldexe = mode_is_executable(oldmode)
 
667
                oldkind = mode_kind(oldmode)
 
668
            else:
 
669
                oldexe = False
 
670
                oldkind = None
 
671
            if oldpath == u'':
121
672
                oldparent = None
122
 
                oldname = oldpath
 
673
                oldname = ''
123
674
            else:
 
675
                (oldparentpath, oldname) = osutils.split(oldpath)
124
676
                oldparent = mapping.generate_file_id(oldparentpath)
125
677
            fileid = mapping.generate_file_id(oldpath)
126
678
        if newpath is None:
128
680
            newkind = None
129
681
            newname = None
130
682
            newparent = None
 
683
            newversioned = False
131
684
        else:
132
 
            newexe = mode_is_executable(newmode)
133
 
            newkind = mode_kind(newmode)
134
 
            try:
135
 
                newparentpath, newname = newpath.rsplit("/", 1)
136
 
            except ValueError:
 
685
            newversioned = (newpath not in target_extras)
 
686
            if newmode:
 
687
                newexe = mode_is_executable(newmode)
 
688
                newkind = mode_kind(newmode)
 
689
            else:
 
690
                newexe = False
 
691
                newkind = None
 
692
            newpath = newpath.decode("utf-8")
 
693
            if newpath == u'':
137
694
                newparent = None
138
 
                newname = newpath
 
695
                newname = u''
139
696
            else:
 
697
                newparentpath, newname = osutils.split(newpath)
140
698
                newparent = mapping.generate_file_id(newparentpath)
 
699
        if (not include_unchanged and
 
700
            oldkind == 'directory' and newkind == 'directory' and
 
701
            oldpath == newpath):
 
702
            continue
141
703
        yield (fileid, (oldpath, newpath), (oldsha != newsha),
142
 
             (oldpath is not None, newpath is not None),
 
704
             (oldversioned, newversioned),
143
705
             (oldparent, newparent), (oldname, newname),
144
706
             (oldkind, newkind), (oldexe, newexe))
145
707
 
146
708
 
147
 
class InterGitRevisionTrees(tree.InterTree):
148
 
    """InterTree that works between two git revision trees."""
 
709
class InterGitTrees(_mod_tree.InterTree):
 
710
    """InterTree that works between two git trees."""
149
711
 
150
712
    _matching_from_tree_format = None
151
713
    _matching_to_tree_format = None
159
721
    def compare(self, want_unchanged=False, specific_files=None,
160
722
                extra_trees=None, require_versioned=False, include_root=False,
161
723
                want_unversioned=False):
162
 
        if self.source._repository._git.object_store != self.target._repository._git.object_store:
163
 
            raise AssertionError
164
 
        changes = self.source._repository._git.object_store.tree_changes(
165
 
            self.source.tree, self.target.tree, want_unchanged=want_unchanged)
166
 
        source_fileid_map = self.source.mapping.get_fileid_map(
167
 
            self.source._repository._git.object_store.__getitem__,
168
 
            self.source.tree)
169
 
        target_fileid_map = self.target.mapping.get_fileid_map(
170
 
            self.target._repository._git.object_store.__getitem__,
171
 
            self.target.tree)
172
 
        return tree_delta_from_git_changes(changes, self.target.mapping,
173
 
            (source_fileid_map, target_fileid_map),
174
 
            specific_file=specific_files)
 
724
        with self.lock_read():
 
725
            changes, target_extras = self._iter_git_changes(
 
726
                    want_unchanged=want_unchanged,
 
727
                    require_versioned=require_versioned,
 
728
                    specific_files=specific_files,
 
729
                    extra_trees=extra_trees,
 
730
                    want_unversioned=want_unversioned)
 
731
            source_fileid_map = self.source._fileid_map
 
732
            target_fileid_map = self.target._fileid_map
 
733
            return tree_delta_from_git_changes(changes, self.target.mapping,
 
734
                (source_fileid_map, target_fileid_map),
 
735
                specific_files=specific_files, include_root=include_root,
 
736
                target_extras=target_extras)
175
737
 
176
738
    def iter_changes(self, include_unchanged=False, specific_files=None,
177
 
        pb=None, extra_trees=[], require_versioned=True,
178
 
        want_unversioned=False):
 
739
                     pb=None, extra_trees=[], require_versioned=True,
 
740
                     want_unversioned=False):
 
741
        with self.lock_read():
 
742
            changes, target_extras = self._iter_git_changes(
 
743
                    want_unchanged=include_unchanged,
 
744
                    require_versioned=require_versioned,
 
745
                    specific_files=specific_files,
 
746
                    extra_trees=extra_trees,
 
747
                    want_unversioned=want_unversioned)
 
748
            return changes_from_git_changes(
 
749
                    changes, self.target.mapping,
 
750
                    specific_files=specific_files,
 
751
                    include_unchanged=include_unchanged,
 
752
                    target_extras=target_extras)
 
753
 
 
754
    def _iter_git_changes(self, want_unchanged=False, specific_files=None,
 
755
            require_versioned=False, extra_trees=None,
 
756
            want_unversioned=False):
 
757
        raise NotImplementedError(self._iter_git_changes)
 
758
 
 
759
 
 
760
class InterGitRevisionTrees(InterGitTrees):
 
761
    """InterTree that works between two git revision trees."""
 
762
 
 
763
    _matching_from_tree_format = None
 
764
    _matching_to_tree_format = None
 
765
    _test_mutable_trees_to_test_trees = None
 
766
 
 
767
    @classmethod
 
768
    def is_compatible(cls, source, target):
 
769
        return (isinstance(source, GitRevisionTree) and
 
770
                isinstance(target, GitRevisionTree))
 
771
 
 
772
    def _iter_git_changes(self, want_unchanged=False, specific_files=None,
 
773
            require_versioned=True, extra_trees=None,
 
774
            want_unversioned=False):
 
775
        trees = [self.source]
 
776
        if extra_trees is not None:
 
777
            trees.extend(extra_trees)
 
778
        if specific_files is not None:
 
779
            specific_files = self.target.find_related_paths_across_trees(
 
780
                    specific_files, trees,
 
781
                    require_versioned=require_versioned)
 
782
 
179
783
        if self.source._repository._git.object_store != self.target._repository._git.object_store:
 
784
            store = OverlayObjectStore([self.source._repository._git.object_store,
 
785
                                        self.target._repository._git.object_store])
 
786
        else:
 
787
            store = self.source._repository._git.object_store
 
788
        return self.source._repository._git.object_store.tree_changes(
 
789
            self.source.tree, self.target.tree, want_unchanged=want_unchanged,
 
790
            include_trees=True, change_type_same=True), set()
 
791
 
 
792
 
 
793
_mod_tree.InterTree.register_optimiser(InterGitRevisionTrees)
 
794
 
 
795
 
 
796
class MutableGitIndexTree(mutabletree.MutableTree):
 
797
 
 
798
    def __init__(self):
 
799
        self._lock_mode = None
 
800
        self._lock_count = 0
 
801
        self._versioned_dirs = None
 
802
        self._index_dirty = False
 
803
 
 
804
    def is_versioned(self, path):
 
805
        with self.lock_read():
 
806
            path = path.rstrip('/').encode('utf-8')
 
807
            (index, subpath) = self._lookup_index(path)
 
808
            return (subpath in index or self._has_dir(path))
 
809
 
 
810
    def _has_dir(self, path):
 
811
        if path == "":
 
812
            return True
 
813
        if self._versioned_dirs is None:
 
814
            self._load_dirs()
 
815
        return path in self._versioned_dirs
 
816
 
 
817
    def _load_dirs(self):
 
818
        if self._lock_mode is None:
 
819
            raise errors.ObjectNotLocked(self)
 
820
        self._versioned_dirs = set()
 
821
        # TODO(jelmer): Browse over all indexes
 
822
        for p, i in self._recurse_index_entries():
 
823
            self._ensure_versioned_dir(posixpath.dirname(p))
 
824
 
 
825
    def _ensure_versioned_dir(self, dirname):
 
826
        if dirname in self._versioned_dirs:
 
827
            return
 
828
        if dirname != "":
 
829
            self._ensure_versioned_dir(posixpath.dirname(dirname))
 
830
        self._versioned_dirs.add(dirname)
 
831
 
 
832
    def path2id(self, path):
 
833
        with self.lock_read():
 
834
            path = path.rstrip('/')
 
835
            if self.is_versioned(path.rstrip('/')):
 
836
                return self._fileid_map.lookup_file_id(path.encode("utf-8"))
 
837
            return None
 
838
 
 
839
    def has_id(self, file_id):
 
840
        try:
 
841
            self.id2path(file_id)
 
842
        except errors.NoSuchId:
 
843
            return False
 
844
        else:
 
845
            return True
 
846
 
 
847
    def id2path(self, file_id):
 
848
        if file_id is None:
 
849
            return ''
 
850
        if type(file_id) is not bytes:
 
851
            raise TypeError(file_id)
 
852
        with self.lock_read():
 
853
            try:
 
854
                path = self._fileid_map.lookup_path(file_id)
 
855
            except ValueError:
 
856
                raise errors.NoSuchId(self, file_id)
 
857
            path = path.decode('utf-8')
 
858
            if self.is_versioned(path):
 
859
                return path
 
860
            raise errors.NoSuchId(self, file_id)
 
861
 
 
862
    def _set_root_id(self, file_id):
 
863
        self._fileid_map.set_file_id("", file_id)
 
864
 
 
865
    def get_root_id(self):
 
866
        return self.path2id("")
 
867
 
 
868
    def _add(self, files, ids, kinds):
 
869
        for (path, file_id, kind) in zip(files, ids, kinds):
 
870
            if file_id is not None:
 
871
                raise workingtree.SettingFileIdUnsupported()
 
872
            path, can_access = osutils.normalized_filename(path)
 
873
            if not can_access:
 
874
                raise errors.InvalidNormalization(path)
 
875
            self._index_add_entry(path, kind)
 
876
 
 
877
    def _read_submodule_head(self, path):
 
878
        raise NotImplementedError(self._read_submodule_head)
 
879
 
 
880
    def _lookup_index(self, encoded_path):
 
881
        if not isinstance(encoded_path, bytes):
 
882
            raise TypeError(encoded_path)
 
883
        # TODO(jelmer): Look in other indexes
 
884
        return self.index, encoded_path
 
885
 
 
886
    def _index_del_entry(self, index, path):
 
887
        del index[path]
 
888
        # TODO(jelmer): Keep track of dirty per index
 
889
        self._index_dirty = True
 
890
 
 
891
    def _index_add_entry(self, path, kind, flags=0, reference_revision=None):
 
892
        if kind == "directory":
 
893
            # Git indexes don't contain directories
 
894
            return
 
895
        if kind == "file":
 
896
            blob = Blob()
 
897
            try:
 
898
                file, stat_val = self.get_file_with_stat(path)
 
899
            except (errors.NoSuchFile, IOError):
 
900
                # TODO: Rather than come up with something here, use the old index
 
901
                file = BytesIO()
 
902
                stat_val = os.stat_result(
 
903
                    (stat.S_IFREG | 0o644, 0, 0, 0, 0, 0, 0, 0, 0, 0))
 
904
            blob.set_raw_string(file.read())
 
905
            # Add object to the repository if it didn't exist yet
 
906
            if not blob.id in self.store:
 
907
                self.store.add_object(blob)
 
908
            hexsha = blob.id
 
909
        elif kind == "symlink":
 
910
            blob = Blob()
 
911
            try:
 
912
                stat_val = self._lstat(path)
 
913
            except EnvironmentError:
 
914
                # TODO: Rather than come up with something here, use the
 
915
                # old index
 
916
                stat_val = os.stat_result(
 
917
                    (stat.S_IFLNK, 0, 0, 0, 0, 0, 0, 0, 0, 0))
 
918
            blob.set_raw_string(
 
919
                self.get_symlink_target(path).encode("utf-8"))
 
920
            # Add object to the repository if it didn't exist yet
 
921
            if not blob.id in self.store:
 
922
                self.store.add_object(blob)
 
923
            hexsha = blob.id
 
924
        elif kind == "tree-reference":
 
925
            if reference_revision is not None:
 
926
                hexsha = self.branch.lookup_bzr_revision_id(reference_revision)[0]
 
927
            else:
 
928
                hexsha = self._read_submodule_head(path)
 
929
                if hexsha is None:
 
930
                    raise errors.NoCommits(path)
 
931
            try:
 
932
                stat_val = self._lstat(path)
 
933
            except EnvironmentError:
 
934
                stat_val = os.stat_result(
 
935
                    (S_IFGITLINK, 0, 0, 0, 0, 0, 0, 0, 0, 0))
 
936
            stat_val = os.stat_result((S_IFGITLINK, ) + stat_val[1:])
 
937
        else:
 
938
            raise AssertionError("unknown kind '%s'" % kind)
 
939
        # Add an entry to the index or update the existing entry
 
940
        ensure_normalized_path(path)
 
941
        encoded_path = path.encode("utf-8")
 
942
        if b'\r' in encoded_path or b'\n' in encoded_path:
 
943
            # TODO(jelmer): Why do we need to do this?
 
944
            trace.mutter('ignoring path with invalid newline in it: %r', path)
 
945
            return
 
946
        (index, index_path) = self._lookup_index(encoded_path)
 
947
        index[index_path] = index_entry_from_stat(stat_val, hexsha, flags)
 
948
        self._index_dirty = True
 
949
        if self._versioned_dirs is not None:
 
950
            self._ensure_versioned_dir(index_path)
 
951
 
 
952
    def _recurse_index_entries(self, index=None, basepath=""):
 
953
        # Iterate over all index entries
 
954
        with self.lock_read():
 
955
            if index is None:
 
956
                index = self.index
 
957
            for path, value in index.iteritems():
 
958
                yield (posixpath.join(basepath, path), value)
 
959
                (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = value
 
960
                if S_ISGITLINK(mode):
 
961
                    pass # TODO(jelmer): dive into submodule
 
962
 
 
963
 
 
964
    def iter_entries_by_dir(self, specific_files=None, yield_parents=False):
 
965
        if yield_parents:
 
966
            raise NotImplementedError(self.iter_entries_by_dir)
 
967
        with self.lock_read():
 
968
            if specific_files is not None:
 
969
                specific_files = set(specific_files)
 
970
            else:
 
971
                specific_files = None
 
972
            root_ie = self._get_dir_ie(u"", None)
 
973
            ret = {}
 
974
            if specific_files is None or u"" in specific_files:
 
975
                ret[(None, u"")] = root_ie
 
976
            dir_ids = {u"": root_ie.file_id}
 
977
            for path, value in self._recurse_index_entries():
 
978
                if self.mapping.is_special_file(path):
 
979
                    continue
 
980
                path = path.decode("utf-8")
 
981
                if specific_files is not None and not path in specific_files:
 
982
                    continue
 
983
                (parent, name) = posixpath.split(path)
 
984
                try:
 
985
                    file_ie = self._get_file_ie(name, path, value, None)
 
986
                except errors.NoSuchFile:
 
987
                    continue
 
988
                if yield_parents or specific_files is None:
 
989
                    for (dir_path, dir_ie) in self._add_missing_parent_ids(parent,
 
990
                            dir_ids):
 
991
                        ret[(posixpath.dirname(dir_path), dir_path)] = dir_ie
 
992
                file_ie.parent_id = self.path2id(parent)
 
993
                ret[(posixpath.dirname(path), path)] = file_ie
 
994
            return ((path, ie) for ((_, path), ie) in sorted(ret.items()))
 
995
 
 
996
    def iter_references(self):
 
997
        # TODO(jelmer): Implement a more efficient version of this
 
998
        for path, entry in self.iter_entries_by_dir():
 
999
            if entry.kind == 'tree-reference':
 
1000
                yield path, self.mapping.generate_file_id(b'')
 
1001
 
 
1002
    def _get_dir_ie(self, path, parent_id):
 
1003
        file_id = self.path2id(path)
 
1004
        return GitTreeDirectory(file_id,
 
1005
            posixpath.basename(path).strip("/"), parent_id)
 
1006
 
 
1007
    def _get_file_ie(self, name, path, value, parent_id):
 
1008
        if type(name) is not unicode:
 
1009
            raise TypeError(name)
 
1010
        if type(path) is not unicode:
 
1011
            raise TypeError(path)
 
1012
        if not isinstance(value, tuple) or len(value) != 10:
 
1013
            raise TypeError(value)
 
1014
        (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = value
 
1015
        file_id = self.path2id(path)
 
1016
        if type(file_id) != str:
180
1017
            raise AssertionError
181
 
        changes = self.source._repository._git.object_store.tree_changes(
182
 
            self.source.tree, self.target.tree,
183
 
            want_unchanged=include_unchanged)
184
 
        return changes_from_git_changes(changes, self.target.mapping,
185
 
            specific_file=specific_files)
186
 
 
187
 
 
188
 
tree.InterTree.register_optimiser(InterGitRevisionTrees)
 
1018
        kind = mode_kind(mode)
 
1019
        ie = entry_factory[kind](file_id, name, parent_id)
 
1020
        if kind == 'symlink':
 
1021
            ie.symlink_target = self.get_symlink_target(path, file_id)
 
1022
        elif kind == 'tree-reference':
 
1023
            ie.reference_revision = self.get_reference_revision(path, file_id)
 
1024
        else:
 
1025
            try:
 
1026
                data = self.get_file_text(path, file_id)
 
1027
            except errors.NoSuchFile:
 
1028
                data = None
 
1029
            except IOError as e:
 
1030
                if e.errno != errno.ENOENT:
 
1031
                    raise
 
1032
                data = None
 
1033
            if data is None:
 
1034
                data = self.branch.repository._git.object_store[sha].data
 
1035
            ie.text_sha1 = osutils.sha_string(data)
 
1036
            ie.text_size = len(data)
 
1037
            ie.executable = bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
 
1038
        return ie
 
1039
 
 
1040
    def _add_missing_parent_ids(self, path, dir_ids):
 
1041
        if path in dir_ids:
 
1042
            return []
 
1043
        parent = posixpath.dirname(path).strip("/")
 
1044
        ret = self._add_missing_parent_ids(parent, dir_ids)
 
1045
        parent_id = dir_ids[parent]
 
1046
        ie = self._get_dir_ie(path, parent_id)
 
1047
        dir_ids[path] = ie.file_id
 
1048
        ret.append((path, ie))
 
1049
        return ret
 
1050
 
 
1051
    def _comparison_data(self, entry, path):
 
1052
        if entry is None:
 
1053
            return None, False, None
 
1054
        return entry.kind, entry.executable, None
 
1055
 
 
1056
    def _unversion_path(self, path):
 
1057
        if self._lock_mode is None:
 
1058
            raise errors.ObjectNotLocked(self)
 
1059
        encoded_path = path.encode("utf-8")
 
1060
        count = 0
 
1061
        (index, subpath) = self._lookup_index(encoded_path)
 
1062
        try:
 
1063
            self._index_del_entry(index, encoded_path)
 
1064
        except KeyError:
 
1065
            # A directory, perhaps?
 
1066
            # TODO(jelmer): Deletes that involve submodules?
 
1067
            for p in list(index):
 
1068
                if p.startswith(subpath+b"/"):
 
1069
                    count += 1
 
1070
                    self._index_del_entry(index, p)
 
1071
        else:
 
1072
            count = 1
 
1073
        self._versioned_dirs = None
 
1074
        return count
 
1075
 
 
1076
    def unversion(self, paths, file_ids=None):
 
1077
        with self.lock_tree_write():
 
1078
            for path in paths:
 
1079
                if self._unversion_path(path) == 0:
 
1080
                    raise errors.NoSuchFile(path)
 
1081
            self._versioned_dirs = None
 
1082
            self.flush()
 
1083
 
 
1084
    def flush(self):
 
1085
        pass
 
1086
 
 
1087
    def update_basis_by_delta(self, revid, delta):
 
1088
        # TODO(jelmer): This shouldn't be called, it's inventory specific.
 
1089
        for (old_path, new_path, file_id, ie) in delta:
 
1090
            if old_path is not None:
 
1091
                (index, old_subpath) = self._lookup_index(old_path.encode('utf-8'))
 
1092
                if old_subpath in index:
 
1093
                    self._index_del_entry(index, old_subpath)
 
1094
                    self._versioned_dirs = None
 
1095
            if new_path is not None and ie.kind != 'directory':
 
1096
                self._index_add_entry(new_path, ie.kind)
 
1097
        self.flush()
 
1098
        self._set_merges_from_parent_ids([])
 
1099
 
 
1100
    def move(self, from_paths, to_dir=None, after=None):
 
1101
        rename_tuples = []
 
1102
        with self.lock_tree_write():
 
1103
            to_abs = self.abspath(to_dir)
 
1104
            if not os.path.isdir(to_abs):
 
1105
                raise errors.BzrMoveFailedError('', to_dir,
 
1106
                    errors.NotADirectory(to_abs))
 
1107
 
 
1108
            for from_rel in from_paths:
 
1109
                from_tail = os.path.split(from_rel)[-1]
 
1110
                to_rel = os.path.join(to_dir, from_tail)
 
1111
                self.rename_one(from_rel, to_rel, after=after)
 
1112
                rename_tuples.append((from_rel, to_rel))
 
1113
            self.flush()
 
1114
            return rename_tuples
 
1115
 
 
1116
    def rename_one(self, from_rel, to_rel, after=None):
 
1117
        from_path = from_rel.encode("utf-8")
 
1118
        to_rel, can_access = osutils.normalized_filename(to_rel)
 
1119
        if not can_access:
 
1120
            raise errors.InvalidNormalization(to_rel)
 
1121
        to_path = to_rel.encode("utf-8")
 
1122
        with self.lock_tree_write():
 
1123
            if not after:
 
1124
                # Perhaps it's already moved?
 
1125
                after = (
 
1126
                    not self.has_filename(from_rel) and
 
1127
                    self.has_filename(to_rel) and
 
1128
                    not self.is_versioned(to_rel))
 
1129
            if after:
 
1130
                if not self.has_filename(to_rel):
 
1131
                    raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1132
                        errors.NoSuchFile(to_rel))
 
1133
                if self.basis_tree().is_versioned(to_rel):
 
1134
                    raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1135
                        errors.AlreadyVersionedError(to_rel))
 
1136
 
 
1137
                kind = self.kind(to_rel)
 
1138
            else:
 
1139
                try:
 
1140
                    to_kind = self.kind(to_rel)
 
1141
                except errors.NoSuchFile:
 
1142
                    exc_type = errors.BzrRenameFailedError
 
1143
                    to_kind = None
 
1144
                else:
 
1145
                    exc_type = errors.BzrMoveFailedError
 
1146
                if self.is_versioned(to_rel):
 
1147
                    raise exc_type(from_rel, to_rel,
 
1148
                        errors.AlreadyVersionedError(to_rel))
 
1149
                if not self.has_filename(from_rel):
 
1150
                    raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1151
                        errors.NoSuchFile(from_rel))
 
1152
                kind = self.kind(from_rel)
 
1153
                if not self.is_versioned(from_rel) and kind != 'directory':
 
1154
                    raise exc_type(from_rel, to_rel,
 
1155
                        errors.NotVersionedError(from_rel))
 
1156
                if self.has_filename(to_rel):
 
1157
                    raise errors.RenameFailedFilesExist(
 
1158
                        from_rel, to_rel, errors.FileExists(to_rel))
 
1159
 
 
1160
                kind = self.kind(from_rel)
 
1161
 
 
1162
            if not after and kind != 'directory':
 
1163
                (index, from_subpath) = self._lookup_index(from_path)
 
1164
                if from_subpath not in index:
 
1165
                    # It's not a file
 
1166
                    raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1167
                        errors.NotVersionedError(path=from_rel))
 
1168
 
 
1169
            if not after:
 
1170
                try:
 
1171
                    self._rename_one(from_rel, to_rel)
 
1172
                except OSError as e:
 
1173
                    if e.errno == errno.ENOENT:
 
1174
                        raise errors.BzrMoveFailedError(from_rel, to_rel,
 
1175
                            errors.NoSuchFile(to_rel))
 
1176
                    raise
 
1177
            if kind != 'directory':
 
1178
                (index, from_index_path) = self._lookup_index(from_path)
 
1179
                try:
 
1180
                    self._index_del_entry(index, from_path)
 
1181
                except KeyError:
 
1182
                    pass
 
1183
                self._index_add_entry(to_rel, kind)
 
1184
            else:
 
1185
                todo = [(p, i) for (p, i) in self._recurse_index_entries() if p.startswith(from_path+'/')]
 
1186
                for child_path, child_value in todo:
 
1187
                    (child_to_index, child_to_index_path) = self._lookup_index(posixpath.join(to_path, posixpath.relpath(child_path, from_path)))
 
1188
                    child_to_index[child_to_index_path] = child_value
 
1189
                    # TODO(jelmer): Mark individual index as dirty
 
1190
                    self._index_dirty = True
 
1191
                    (child_from_index, child_from_index_path) = self._lookup_index(child_path)
 
1192
                    self._index_del_entry(child_from_index, child_from_index_path)
 
1193
 
 
1194
            self._versioned_dirs = None
 
1195
            self.flush()
 
1196
 
 
1197
    def find_related_paths_across_trees(self, paths, trees=[],
 
1198
            require_versioned=True):
 
1199
        if paths is None:
 
1200
            return None
 
1201
 
 
1202
        if require_versioned:
 
1203
            trees = [self] + (trees if trees is not None else [])
 
1204
            unversioned = set()
 
1205
            for p in paths:
 
1206
                for t in trees:
 
1207
                    if t.is_versioned(p):
 
1208
                        break
 
1209
                else:
 
1210
                    unversioned.add(p)
 
1211
            if unversioned:
 
1212
                raise errors.PathsNotVersionedError(unversioned)
 
1213
 
 
1214
        return filter(self.is_versioned, paths)
 
1215
 
 
1216
    def path_content_summary(self, path):
 
1217
        """See Tree.path_content_summary."""
 
1218
        try:
 
1219
            stat_result = self._lstat(path)
 
1220
        except OSError as e:
 
1221
            if getattr(e, 'errno', None) == errno.ENOENT:
 
1222
                # no file.
 
1223
                return ('missing', None, None, None)
 
1224
            # propagate other errors
 
1225
            raise
 
1226
        kind = mode_kind(stat_result.st_mode)
 
1227
        if kind == 'file':
 
1228
            return self._file_content_summary(path, stat_result)
 
1229
        elif kind == 'directory':
 
1230
            # perhaps it looks like a plain directory, but it's really a
 
1231
            # reference.
 
1232
            if self._directory_is_tree_reference(path):
 
1233
                kind = 'tree-reference'
 
1234
            return kind, None, None, None
 
1235
        elif kind == 'symlink':
 
1236
            target = osutils.readlink(self.abspath(path))
 
1237
            return ('symlink', None, None, target)
 
1238
        else:
 
1239
            return (kind, None, None, None)
 
1240
 
 
1241
    def kind(self, relpath, file_id=None):
 
1242
        kind = osutils.file_kind(self.abspath(relpath))
 
1243
        if kind == 'directory':
 
1244
            (index, index_path) = self._lookup_index(relpath.encode('utf-8'))
 
1245
            try:
 
1246
                mode = index[index_path].mode
 
1247
            except KeyError:
 
1248
                return kind
 
1249
            else:
 
1250
                if S_ISGITLINK(mode):
 
1251
                    return 'tree-reference'
 
1252
                return 'directory'
 
1253
        else:
 
1254
            return kind