1
# Copyright (C) 2009-2018 Jelmer Vernooij <jelmer@jelmer.uk>
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20
from __future__ import absolute_import
23
from io import BytesIO
26
from dulwich.index import (
27
index_entry_from_stat,
29
from dulwich.object_store import (
33
from dulwich.objects import (
44
controldir as _mod_controldir,
55
from ...revision import NULL_REVISION
57
from .mapping import (
65
class GitTreeDirectory(_mod_tree.TreeDirectory):
67
__slots__ = ['file_id', 'name', 'parent_id', 'children']
69
def __init__(self, file_id, name, parent_id):
70
self.file_id = file_id
72
self.parent_id = parent_id
85
return self.__class__(
86
self.file_id, self.name, self.parent_id)
89
return "%s(file_id=%r, name=%r, parent_id=%r)" % (
90
self.__class__.__name__, self.file_id, self.name,
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)
100
class GitTreeFile(_mod_tree.TreeFile):
102
__slots__ = ['file_id', 'name', 'parent_id', 'text_size', 'text_sha1',
105
def __init__(self, file_id, name, parent_id, text_size=None,
106
text_sha1=None, executable=None):
107
self.file_id = file_id
109
self.parent_id = parent_id
110
self.text_size = text_size
111
self.text_sha1 = text_sha1
112
self.executable = executable
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)
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)
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
141
class GitTreeSymlink(_mod_tree.TreeLink):
143
__slots__ = ['file_id', 'name', 'parent_id', 'symlink_target']
145
def __init__(self, file_id, name, parent_id,
146
symlink_target=None):
147
self.file_id = file_id
149
self.parent_id = parent_id
150
self.symlink_target = symlink_target
157
def executable(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,
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)
177
return self.__class__(
178
self.file_id, self.name, self.parent_id,
182
class GitTreeSubmodule(_mod_tree.TreeLink):
184
__slots__ = ['file_id', 'name', 'parent_id', 'reference_revision']
186
def __init__(self, file_id, name, parent_id, reference_revision=None):
187
self.file_id = file_id
189
self.parent_id = parent_id
190
self.reference_revision = reference_revision
194
return 'tree-reference'
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)
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)
209
return self.__class__(
210
self.file_id, self.name, self.parent_id,
211
self.reference_revision)
215
'directory': GitTreeDirectory,
217
'symlink': GitTreeSymlink,
218
'tree-reference': GitTreeSubmodule,
222
def ensure_normalized_path(path):
223
"""Check whether path is normalized.
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.
229
norm_path, can_access = osutils.normalized_filename(path)
230
if norm_path != path:
234
raise errors.InvalidNormalization(path)
238
class GitRevisionTree(revisiontree.RevisionTree):
239
"""Revision tree implementation based on Git objects."""
241
def __init__(self, repository, revision_id):
242
self._revision_id = revision_id
243
self._repository = repository
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:
250
self.mapping = default_mapping
251
self._fileid_map = GitFileIdMap(
256
commit = self.store[self.commit_id]
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)
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()
267
def supports_rename_tracking(self):
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:
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)
278
def get_file_mtime(self, path, file_id=None):
280
revid = self.get_file_revision(path, file_id)
282
raise _mod_tree.FileTimestampUnavailable(path)
284
rev = self._repository.get_revision(revid)
285
except errors.NoSuchRevision:
286
raise _mod_tree.FileTimestampUnavailable(path)
289
def id2path(self, file_id):
291
path = self._fileid_map.lookup_path(file_id)
293
raise errors.NoSuchId(self, file_id)
294
path = path.decode('utf-8')
295
if self.is_versioned(path):
297
raise errors.NoSuchId(self, file_id)
299
def is_versioned(self, path):
300
return self.has_filename(path)
302
def path2id(self, path):
303
if self.mapping.is_special_file(path):
305
return self._fileid_map.lookup_file_id(path.encode('utf-8'))
307
def all_file_ids(self):
308
return set(self._fileid_map.all_file_ids())
310
def all_versioned_paths(self):
312
todo = set([(store, '', self.tree)])
314
(store, path, tree_id) = todo.pop()
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))
326
def get_root_id(self):
327
if self.tree is None:
329
return self.path2id("")
331
def has_or_had_id(self, file_id):
333
path = self.id2path(file_id)
334
except errors.NoSuchId:
338
def has_id(self, file_id):
340
path = self.id2path(file_id)
341
except errors.NoSuchId:
343
return self.has_filename(path)
345
def _lookup_path(self, path):
346
if self.tree is None:
347
raise errors.NoSuchFile(path)
349
(mode, hexsha) = tree_lookup_path(self.store.__getitem__, self.tree,
350
path.encode('utf-8'))
352
raise errors.NoSuchFile(self, path)
354
return (self.store, mode, hexsha)
356
def is_executable(self, path, file_id=None):
357
(store, mode, hexsha) = self._lookup_path(path)
359
# the tree root is a directory
361
return mode_is_executable(mode)
363
def kind(self, path, file_id=None):
364
(store, mode, hexsha) = self._lookup_path(path)
366
# the tree root is a directory
368
return mode_kind(mode)
370
def has_filename(self, path):
372
self._lookup_path(path)
373
except errors.NoSuchFile:
378
def list_files(self, include_root=False, from_dir=None, recursive=True):
379
if self.tree is None:
383
(store, mode, hexsha) = self._lookup_path(from_dir)
384
if mode is None: # Root
385
root_ie = self._get_dir_ie(b"", None)
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)
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)
397
if root_ie.kind == 'directory':
398
todo.add((store, from_dir.encode("utf-8"), hexsha, root_ie.file_id))
400
(store, path, hexsha, parent_id) = todo.pop()
402
for name, mode, hexsha in tree.iteritems():
403
if self.mapping.is_special_file(name):
405
child_path = posixpath.join(path, name)
406
if stat.S_ISDIR(mode):
407
ie = self._get_dir_ie(child_path, parent_id)
409
todo.add((store, child_path, hexsha, ie.file_id))
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
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)
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)
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)
438
def iter_child_entries(self, path, file_id=None):
439
(store, mode, tree_sha) = self._lookup_path(path)
441
if not stat.S_ISDIR(mode):
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):
450
child_path = posixpath.join(encoded_path, name)
451
if stat.S_ISDIR(mode):
452
yield self._get_dir_ie(child_path, file_id)
454
yield self._get_file_ie(store, child_path, name, mode, hexsha,
457
def iter_entries_by_dir(self, specific_files=None, yield_parents=False):
458
if self.tree is None:
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
467
specific_files = set([p.encode('utf-8') for p in specific_files])
468
todo = set([(self.store, "", self.tree, None)])
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):
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,
488
def get_revision_id(self):
489
"""See RevisionTree.get_revision_id."""
490
return self._revision_id
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))
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)
501
def get_file_text(self, path, file_id=None):
502
"""See RevisionTree.get_file_text."""
503
(store, mode, hexsha) = self._lookup_path(path)
504
if stat.S_ISREG(mode):
505
return store[hexsha].data
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')
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)
526
def _comparison_data(self, entry, path):
528
return None, False, None
529
return entry.kind, entry.executable, None
531
def path_content_summary(self, path):
532
"""See Tree.path_content_summary."""
534
(store, mode, hexsha) = self._lookup_path(path)
535
except errors.NoSuchFile:
536
return ('missing', None, None, None)
537
kind = mode_kind(mode)
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))
549
return (kind, None, None, None)
551
def find_related_paths_across_trees(self, paths, trees=[],
552
require_versioned=True):
555
if require_versioned:
556
trees = [self] + (trees if trees is not None else [])
560
if t.is_versioned(p):
565
raise errors.PathsNotVersionedError(unversioned)
566
return filter(self.is_versioned, paths)
568
def _iter_tree_contents(self, include_trees=False):
569
if self.tree is None:
571
return self.store.iter_tree_contents(
572
self.tree, include_trees=include_trees)
575
def tree_delta_from_git_changes(changes, mapping,
576
fileid_maps, specific_files=None,
577
require_versioned=False, include_root=False,
579
"""Create a TreeDelta from two git trees.
581
source and target are iterators over tuples with:
582
(filename, sha, mode)
584
(old_fileid_map, new_fileid_map) = fileid_maps
585
if target_extras is None:
586
target_extras = set()
587
ret = delta.TreeDelta()
588
for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
589
if newpath == u'' and not include_root:
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))):
595
if mapping.is_special_file(oldpath):
597
if mapping.is_special_file(newpath):
599
if oldpath is None and newpath is None:
602
if newpath in target_extras:
603
ret.unversioned.append(
604
(osutils.normalized_filename(newpath)[0], None, mode_kind(newmode)))
606
file_id = new_fileid_map.lookup_file_id(newpath)
607
ret.added.append((newpath.decode('utf-8'), file_id, mode_kind(newmode)))
608
elif newpath is None:
609
file_id = old_fileid_map.lookup_file_id(oldpath)
610
ret.removed.append((oldpath.decode('utf-8'), file_id, mode_kind(oldmode)))
611
elif oldpath != newpath:
612
file_id = old_fileid_map.lookup_file_id(oldpath)
614
(oldpath.decode('utf-8'), newpath.decode('utf-8'), file_id,
615
mode_kind(newmode), (oldsha != newsha),
616
(oldmode != newmode)))
617
elif 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),
622
elif oldsha != newsha or oldmode != newmode:
623
if stat.S_ISDIR(oldmode) and stat.S_ISDIR(newmode):
625
file_id = new_fileid_map.lookup_file_id(newpath)
627
(newpath.decode('utf-8'), file_id, mode_kind(newmode),
628
(oldsha != newsha), (oldmode != newmode)))
630
file_id = new_fileid_map.lookup_file_id(newpath)
631
ret.unchanged.append((newpath.decode('utf-8'), file_id, mode_kind(newmode)))
636
def changes_from_git_changes(changes, mapping, specific_files=None, include_unchanged=False,
638
"""Create a iter_changes-like generator from a git stream.
640
source and target are iterators over tuples with:
641
(filename, sha, mode)
643
if target_extras is None:
644
target_extras = set()
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))):
650
path = (oldpath, newpath)
651
if oldpath is not None and mapping.is_special_file(oldpath):
653
if newpath is not None and mapping.is_special_file(newpath):
656
fileid = mapping.generate_file_id(newpath)
664
oldpath = oldpath.decode("utf-8")
666
oldexe = mode_is_executable(oldmode)
667
oldkind = mode_kind(oldmode)
675
(oldparentpath, oldname) = osutils.split(oldpath)
676
oldparent = mapping.generate_file_id(oldparentpath)
677
fileid = mapping.generate_file_id(oldpath)
685
newversioned = (newpath not in target_extras)
687
newexe = mode_is_executable(newmode)
688
newkind = mode_kind(newmode)
692
newpath = newpath.decode("utf-8")
697
newparentpath, newname = osutils.split(newpath)
698
newparent = mapping.generate_file_id(newparentpath)
699
if (not include_unchanged and
700
oldkind == 'directory' and newkind == 'directory' and
703
yield (fileid, (oldpath, newpath), (oldsha != newsha),
704
(oldversioned, newversioned),
705
(oldparent, newparent), (oldname, newname),
706
(oldkind, newkind), (oldexe, newexe))
709
class InterGitTrees(_mod_tree.InterTree):
710
"""InterTree that works between two git trees."""
712
_matching_from_tree_format = None
713
_matching_to_tree_format = None
714
_test_mutable_trees_to_test_trees = None
717
def is_compatible(cls, source, target):
718
return (isinstance(source, GitRevisionTree) and
719
isinstance(target, GitRevisionTree))
721
def compare(self, want_unchanged=False, specific_files=None,
722
extra_trees=None, require_versioned=False, include_root=False,
723
want_unversioned=False):
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)
738
def iter_changes(self, include_unchanged=False, specific_files=None,
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)
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)
760
class InterGitRevisionTrees(InterGitTrees):
761
"""InterTree that works between two git revision trees."""
763
_matching_from_tree_format = None
764
_matching_to_tree_format = None
765
_test_mutable_trees_to_test_trees = None
768
def is_compatible(cls, source, target):
769
return (isinstance(source, GitRevisionTree) and
770
isinstance(target, GitRevisionTree))
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)
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])
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()
793
_mod_tree.InterTree.register_optimiser(InterGitRevisionTrees)
796
class MutableGitIndexTree(mutabletree.MutableTree):
799
self._lock_mode = None
801
self._versioned_dirs = None
802
self._index_dirty = False
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))
810
def _has_dir(self, path):
813
if self._versioned_dirs is None:
815
return path in self._versioned_dirs
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))
825
def _ensure_versioned_dir(self, dirname):
826
if dirname in self._versioned_dirs:
829
self._ensure_versioned_dir(posixpath.dirname(dirname))
830
self._versioned_dirs.add(dirname)
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"))
839
def has_id(self, file_id):
841
self.id2path(file_id)
842
except errors.NoSuchId:
847
def id2path(self, file_id):
850
if type(file_id) is not bytes:
851
raise TypeError(file_id)
852
with self.lock_read():
854
path = self._fileid_map.lookup_path(file_id)
856
raise errors.NoSuchId(self, file_id)
857
path = path.decode('utf-8')
858
if self.is_versioned(path):
860
raise errors.NoSuchId(self, file_id)
862
def _set_root_id(self, file_id):
863
self._fileid_map.set_file_id("", file_id)
865
def get_root_id(self):
866
return self.path2id("")
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)
874
raise errors.InvalidNormalization(path)
875
self._index_add_entry(path, kind)
877
def _read_submodule_head(self, path):
878
raise NotImplementedError(self._read_submodule_head)
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
886
def _index_del_entry(self, index, path):
888
# TODO(jelmer): Keep track of dirty per index
889
self._index_dirty = True
891
def _index_add_entry(self, path, kind, flags=0, reference_revision=None):
892
if kind == "directory":
893
# Git indexes don't contain directories
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
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)
909
elif kind == "symlink":
912
stat_val = self._lstat(path)
913
except EnvironmentError:
914
# TODO: Rather than come up with something here, use the
916
stat_val = os.stat_result(
917
(stat.S_IFLNK, 0, 0, 0, 0, 0, 0, 0, 0, 0))
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)
924
elif kind == "tree-reference":
925
if reference_revision is not None:
926
hexsha = self.branch.lookup_bzr_revision_id(reference_revision)[0]
928
hexsha = self._read_submodule_head(path)
930
raise errors.NoCommits(path)
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:])
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)
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)
952
def _recurse_index_entries(self, index=None, basepath=""):
953
# Iterate over all index entries
954
with self.lock_read():
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
964
def iter_entries_by_dir(self, specific_files=None, yield_parents=False):
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)
971
specific_files = None
972
root_ie = self._get_dir_ie(u"", None)
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):
980
path = path.decode("utf-8")
981
if specific_files is not None and not path in specific_files:
983
(parent, name) = posixpath.split(path)
985
file_ie = self._get_file_ie(name, path, value, None)
986
except errors.NoSuchFile:
988
if yield_parents or specific_files is None:
989
for (dir_path, dir_ie) in self._add_missing_parent_ids(parent,
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()))
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'')
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)
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:
1017
raise AssertionError
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)
1026
data = self.get_file_text(path, file_id)
1027
except errors.NoSuchFile:
1029
except IOError as e:
1030
if e.errno != errno.ENOENT:
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)
1040
def _add_missing_parent_ids(self, path, dir_ids):
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))
1051
def _comparison_data(self, entry, path):
1053
return None, False, None
1054
return entry.kind, entry.executable, None
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")
1061
(index, subpath) = self._lookup_index(encoded_path)
1063
self._index_del_entry(index, encoded_path)
1065
# A directory, perhaps?
1066
# TODO(jelmer): Deletes that involve submodules?
1067
for p in list(index):
1068
if p.startswith(subpath+b"/"):
1070
self._index_del_entry(index, p)
1073
self._versioned_dirs = None
1076
def unversion(self, paths, file_ids=None):
1077
with self.lock_tree_write():
1079
if self._unversion_path(path) == 0:
1080
raise errors.NoSuchFile(path)
1081
self._versioned_dirs = None
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)
1098
self._set_merges_from_parent_ids([])
1100
def move(self, from_paths, to_dir=None, after=None):
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))
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))
1114
return rename_tuples
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)
1120
raise errors.InvalidNormalization(to_rel)
1121
to_path = to_rel.encode("utf-8")
1122
with self.lock_tree_write():
1124
# Perhaps it's already moved?
1126
not self.has_filename(from_rel) and
1127
self.has_filename(to_rel) and
1128
not self.is_versioned(to_rel))
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))
1137
kind = self.kind(to_rel)
1140
to_kind = self.kind(to_rel)
1141
except errors.NoSuchFile:
1142
exc_type = errors.BzrRenameFailedError
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))
1160
kind = self.kind(from_rel)
1162
if not after and kind != 'directory':
1163
(index, from_subpath) = self._lookup_index(from_path)
1164
if from_subpath not in index:
1166
raise errors.BzrMoveFailedError(from_rel, to_rel,
1167
errors.NotVersionedError(path=from_rel))
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))
1177
if kind != 'directory':
1178
(index, from_index_path) = self._lookup_index(from_path)
1180
self._index_del_entry(index, from_path)
1183
self._index_add_entry(to_rel, kind)
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)
1194
self._versioned_dirs = None
1197
def find_related_paths_across_trees(self, paths, trees=[],
1198
require_versioned=True):
1202
if require_versioned:
1203
trees = [self] + (trees if trees is not None else [])
1207
if t.is_versioned(p):
1212
raise errors.PathsNotVersionedError(unversioned)
1214
return filter(self.is_versioned, paths)
1216
def path_content_summary(self, path):
1217
"""See Tree.path_content_summary."""
1219
stat_result = self._lstat(path)
1220
except OSError as e:
1221
if getattr(e, 'errno', None) == errno.ENOENT:
1223
return ('missing', None, None, None)
1224
# propagate other errors
1226
kind = mode_kind(stat_result.st_mode)
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
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)
1239
return (kind, None, None, None)
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'))
1246
mode = index[index_path].mode
1250
if S_ISGITLINK(mode):
1251
return 'tree-reference'