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 (
52
from ...revision import NULL_REVISION
54
from .mapping import (
62
class GitTreeDirectory(_mod_tree.TreeDirectory):
64
__slots__ = ['file_id', 'name', 'parent_id', 'children']
66
def __init__(self, file_id, name, parent_id):
67
self.file_id = file_id
69
self.parent_id = parent_id
82
return self.__class__(
83
self.file_id, self.name, self.parent_id)
86
return "%s(file_id=%r, name=%r, parent_id=%r)" % (
87
self.__class__.__name__, self.file_id, self.name,
90
def __eq__(self, other):
91
return (self.kind == other.kind and
92
self.file_id == other.file_id and
93
self.name == other.name and
94
self.parent_id == other.parent_id)
97
class GitTreeFile(_mod_tree.TreeFile):
99
__slots__ = ['file_id', 'name', 'parent_id', 'text_size', 'text_sha1',
102
def __init__(self, file_id, name, parent_id, text_size=None,
103
text_sha1=None, executable=None):
104
self.file_id = file_id
106
self.parent_id = parent_id
107
self.text_size = text_size
108
self.text_sha1 = text_sha1
109
self.executable = executable
115
def __eq__(self, other):
116
return (self.kind == other.kind and
117
self.file_id == other.file_id and
118
self.name == other.name and
119
self.parent_id == other.parent_id and
120
self.text_sha1 == other.text_sha1 and
121
self.text_size == other.text_size and
122
self.executable == other.executable)
125
return "%s(file_id=%r, name=%r, parent_id=%r, text_size=%r, text_sha1=%r, executable=%r)" % (
126
type(self).__name__, self.file_id, self.name, self.parent_id,
127
self.text_size, self.text_sha1, self.executable)
130
ret = self.__class__(
131
self.file_id, self.name, self.parent_id)
132
ret.text_sha1 = self.text_sha1
133
ret.text_size = self.text_size
134
ret.executable = self.executable
138
class GitTreeSymlink(_mod_tree.TreeLink):
140
__slots__ = ['file_id', 'name', 'parent_id', 'symlink_target']
142
def __init__(self, file_id, name, parent_id,
143
symlink_target=None):
144
self.file_id = file_id
146
self.parent_id = parent_id
147
self.symlink_target = symlink_target
154
def executable(self):
162
return "%s(file_id=%r, name=%r, parent_id=%r, symlink_target=%r)" % (
163
type(self).__name__, self.file_id, self.name, self.parent_id,
166
def __eq__(self, other):
167
return (self.kind == other.kind and
168
self.file_id == other.file_id and
169
self.name == other.name and
170
self.parent_id == other.parent_id and
171
self.symlink_target == other.symlink_target)
174
return self.__class__(
175
self.file_id, self.name, self.parent_id,
180
'directory': GitTreeDirectory,
182
'symlink': GitTreeSymlink,
186
def ensure_normalized_path(path):
187
"""Check whether path is normalized.
189
:raises InvalidNormalization: When path is not normalized, and cannot be
190
accessed on this platform by the normalized path.
191
:return: The NFC normalised version of path.
193
norm_path, can_access = osutils.normalized_filename(path)
194
if norm_path != path:
198
raise errors.InvalidNormalization(path)
202
class GitRevisionTree(revisiontree.RevisionTree):
203
"""Revision tree implementation based on Git objects."""
205
def __init__(self, repository, revision_id):
206
self._revision_id = revision_id
207
self._repository = repository
208
self.store = repository._git.object_store
209
if type(revision_id) is not str:
210
raise TypeError(revision_id)
211
self.commit_id, self.mapping = repository.lookup_bzr_revision_id(revision_id)
212
if revision_id == NULL_REVISION:
214
self.mapping = default_mapping
215
self._fileid_map = GitFileIdMap(
220
commit = self.store[self.commit_id]
222
raise errors.NoSuchRevision(repository, revision_id)
223
self.tree = commit.tree
224
self._fileid_map = self.mapping.get_fileid_map(self.store.__getitem__, self.tree)
226
def supports_rename_tracking(self):
229
def get_file_revision(self, path, file_id=None):
230
change_scanner = self._repository._file_change_scanner
231
if self.commit_id == ZERO_SHA:
233
(path, commit_id) = change_scanner.find_last_change_revision(
234
path.encode('utf-8'), self.commit_id)
235
return self._repository.lookup_foreign_revision_id(commit_id, self.mapping)
237
def get_file_mtime(self, path, file_id=None):
238
revid = self.get_file_revision(path, file_id)
240
rev = self._repository.get_revision(revid)
241
except errors.NoSuchRevision:
242
raise errors.FileTimestampUnavailable(path)
245
def id2path(self, file_id):
247
path = self._fileid_map.lookup_path(file_id)
249
raise errors.NoSuchId(self, file_id)
250
path = path.decode('utf-8')
251
if self.is_versioned(path):
253
raise errors.NoSuchId(self, file_id)
255
def is_versioned(self, path):
256
return self.has_filename(path)
258
def path2id(self, path):
259
if self.mapping.is_special_file(path):
261
return self._fileid_map.lookup_file_id(path.encode('utf-8'))
263
def all_file_ids(self):
264
return set(self._fileid_map.all_file_ids())
266
def all_versioned_paths(self):
268
todo = set([(store, '', self.tree)])
270
(store, path, tree_id) = todo.pop()
273
tree = store[tree_id]
274
for name, mode, hexsha in tree.items():
275
subpath = posixpath.join(path, name)
276
if stat.S_ISDIR(mode):
277
todo.add((store, subpath, hexsha))
282
def get_root_id(self):
283
if self.tree is None:
285
return self.path2id("")
287
def has_or_had_id(self, file_id):
289
path = self.id2path(file_id)
290
except errors.NoSuchId:
294
def has_id(self, file_id):
296
path = self.id2path(file_id)
297
except errors.NoSuchId:
299
return self.has_filename(path)
301
def _lookup_path(self, path):
302
if self.tree is None:
303
raise errors.NoSuchFile(path)
305
(mode, hexsha) = tree_lookup_path(self.store.__getitem__, self.tree,
306
path.encode('utf-8'))
308
raise errors.NoSuchFile(self, path)
310
return (self.store, mode, hexsha)
312
def is_executable(self, path, file_id=None):
313
(store, mode, hexsha) = self._lookup_path(path)
315
# the tree root is a directory
317
return mode_is_executable(mode)
319
def kind(self, path, file_id=None):
320
(store, mode, hexsha) = self._lookup_path(path)
322
# the tree root is a directory
324
return mode_kind(mode)
326
def has_filename(self, path):
328
self._lookup_path(path)
329
except errors.NoSuchFile:
334
def list_files(self, include_root=False, from_dir=None, recursive=True):
335
if self.tree is None:
339
(store, mode, hexsha) = self._lookup_path(from_dir)
340
if mode is None: # Root
341
root_ie = self._get_dir_ie(b"", None)
343
parent_path = posixpath.dirname(from_dir.encode("utf-8"))
344
parent_id = self._fileid_map.lookup_file_id(parent_path)
345
if mode_kind(mode) == 'directory':
346
root_ie = self._get_dir_ie(from_dir.encode("utf-8"), parent_id)
348
root_ie = self._get_file_ie(store, from_dir.encode("utf-8"),
349
posixpath.basename(from_dir), mode, hexsha)
350
if from_dir != "" or include_root:
351
yield (from_dir, "V", root_ie.kind, root_ie.file_id, root_ie)
353
if root_ie.kind == 'directory':
354
todo.add((store, from_dir.encode("utf-8"), hexsha, root_ie.file_id))
356
(store, path, hexsha, parent_id) = todo.pop()
358
for name, mode, hexsha in tree.iteritems():
359
if self.mapping.is_special_file(name):
361
child_path = posixpath.join(path, name)
362
if stat.S_ISDIR(mode):
363
ie = self._get_dir_ie(child_path, parent_id)
365
todo.add((store, child_path, hexsha, ie.file_id))
367
ie = self._get_file_ie(store, child_path, name, mode, hexsha, parent_id)
368
yield child_path.decode('utf-8'), "V", ie.kind, ie.file_id, ie
370
def _get_file_ie(self, store, path, name, mode, hexsha, parent_id):
371
if type(path) is not bytes:
372
raise TypeError(path)
373
if type(name) is not bytes:
374
raise TypeError(name)
375
kind = mode_kind(mode)
376
file_id = self._fileid_map.lookup_file_id(path)
377
ie = entry_factory[kind](file_id, name.decode("utf-8"), parent_id)
378
if kind == 'symlink':
379
ie.symlink_target = store[hexsha].data.decode('utf-8')
380
elif kind == 'tree-reference':
381
ie.reference_revision = self.mapping.revision_id_foreign_to_bzr(hexsha)
383
data = store[hexsha].data
384
ie.text_sha1 = osutils.sha_string(data)
385
ie.text_size = len(data)
386
ie.executable = mode_is_executable(mode)
389
def _get_dir_ie(self, path, parent_id):
390
file_id = self._fileid_map.lookup_file_id(path)
391
return GitTreeDirectory(file_id,
392
posixpath.basename(path).decode("utf-8"), parent_id)
394
def iter_child_entries(self, path, file_id=None):
395
(store, mode, tree_sha) = self._lookup_path(path)
397
if not stat.S_ISDIR(mode):
400
encoded_path = path.encode('utf-8')
401
file_id = self.path2id(path)
402
tree = store[tree_sha]
403
for name, mode, hexsha in tree.iteritems():
404
if self.mapping.is_special_file(name):
406
child_path = posixpath.join(encoded_path, name)
407
if stat.S_ISDIR(mode):
408
yield self._get_dir_ie(child_path, file_id)
410
yield self._get_file_ie(store, child_path, name, mode, hexsha,
413
def iter_entries_by_dir(self, specific_files=None, yield_parents=False):
414
if self.tree is None:
417
# TODO(jelmer): Support yield parents
418
raise NotImplementedError
419
if specific_files is not None:
420
if specific_files in ([""], []):
421
specific_files = None
423
specific_files = set([p.encode('utf-8') for p in specific_files])
424
todo = set([(self.store, "", self.tree, None)])
426
store, path, tree_sha, parent_id = todo.pop()
427
ie = self._get_dir_ie(path, parent_id)
428
if specific_files is None or path in specific_files:
429
yield path.decode("utf-8"), ie
430
tree = store[tree_sha]
431
for name, mode, hexsha in tree.iteritems():
432
if self.mapping.is_special_file(name):
434
child_path = posixpath.join(path, name)
435
if stat.S_ISDIR(mode):
436
if (specific_files is None or
437
any(filter(lambda p: p.startswith(child_path), specific_files))):
438
todo.add((store, child_path, hexsha, ie.file_id))
439
elif specific_files is None or child_path in specific_files:
440
yield (child_path.decode("utf-8"),
441
self._get_file_ie(store, child_path, name, mode, hexsha,
444
def get_revision_id(self):
445
"""See RevisionTree.get_revision_id."""
446
return self._revision_id
448
def get_file_sha1(self, path, file_id=None, stat_value=None):
449
if self.tree is None:
450
raise errors.NoSuchFile(path)
451
return osutils.sha_string(self.get_file_text(path, file_id))
453
def get_file_verifier(self, path, file_id=None, stat_value=None):
454
(store, mode, hexsha) = self._lookup_path(path)
455
return ("GIT", hexsha)
457
def get_file_text(self, path, file_id=None):
458
"""See RevisionTree.get_file_text."""
459
(store, mode, hexsha) = self._lookup_path(path)
460
if stat.S_ISREG(mode):
461
return store[hexsha].data
465
def get_symlink_target(self, path, file_id=None):
466
"""See RevisionTree.get_symlink_target."""
467
(store, mode, hexsha) = self._lookup_path(path)
468
if stat.S_ISLNK(mode):
469
return store[hexsha].data.decode('utf-8')
473
def _comparison_data(self, entry, path):
475
return None, False, None
476
return entry.kind, entry.executable, None
478
def path_content_summary(self, path):
479
"""See Tree.path_content_summary."""
481
(store, mode, hexsha) = self._lookup_path(path)
482
except errors.NoSuchFile:
483
return ('missing', None, None, None)
484
kind = mode_kind(mode)
486
executable = mode_is_executable(mode)
487
contents = store[hexsha].data
488
return (kind, len(contents), executable, osutils.sha_string(contents))
489
elif kind == 'symlink':
490
return (kind, None, None, store[hexsha].data)
492
return (kind, None, None, None)
494
def find_related_paths_across_trees(self, paths, trees=[],
495
require_versioned=True):
498
if require_versioned:
499
trees = [self] + (trees if trees is not None else [])
503
if t.is_versioned(p):
508
raise errors.PathsNotVersionedError(unversioned)
509
return filter(self.is_versioned, paths)
511
def _iter_tree_contents(self, include_trees=False):
512
if self.tree is None:
514
return self.store.iter_tree_contents(
515
self.tree, include_trees=include_trees)
518
def tree_delta_from_git_changes(changes, mapping,
519
(old_fileid_map, new_fileid_map), specific_files=None,
520
require_versioned=False, include_root=False,
522
"""Create a TreeDelta from two git trees.
524
source and target are iterators over tuples with:
525
(filename, sha, mode)
527
if target_extras is None:
528
target_extras = set()
529
ret = delta.TreeDelta()
530
for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
531
if newpath == u'' and not include_root:
533
if not (specific_files is None or
534
(oldpath is not None and osutils.is_inside_or_parent_of_any(specific_files, oldpath)) or
535
(newpath is not None and osutils.is_inside_or_parent_of_any(specific_files, newpath))):
537
if mapping.is_special_file(oldpath):
539
if mapping.is_special_file(newpath):
541
if oldpath is None and newpath is None:
544
if newpath in target_extras:
545
ret.unversioned.append(
546
(osutils.normalized_filename(newpath)[0], None, mode_kind(newmode)))
548
file_id = new_fileid_map.lookup_file_id(newpath)
549
ret.added.append((newpath.decode('utf-8'), file_id, mode_kind(newmode)))
550
elif newpath is None:
551
file_id = old_fileid_map.lookup_file_id(oldpath)
552
ret.removed.append((oldpath.decode('utf-8'), file_id, mode_kind(oldmode)))
553
elif oldpath != newpath:
554
file_id = old_fileid_map.lookup_file_id(oldpath)
556
(oldpath.decode('utf-8'), newpath.decode('utf-8'), file_id,
557
mode_kind(newmode), (oldsha != newsha),
558
(oldmode != newmode)))
559
elif mode_kind(oldmode) != mode_kind(newmode):
560
file_id = new_fileid_map.lookup_file_id(newpath)
561
ret.kind_changed.append(
562
(newpath.decode('utf-8'), file_id, mode_kind(oldmode),
564
elif oldsha != newsha or oldmode != newmode:
565
if stat.S_ISDIR(oldmode) and stat.S_ISDIR(newmode):
567
file_id = new_fileid_map.lookup_file_id(newpath)
569
(newpath.decode('utf-8'), file_id, mode_kind(newmode),
570
(oldsha != newsha), (oldmode != newmode)))
572
file_id = new_fileid_map.lookup_file_id(newpath)
573
ret.unchanged.append((newpath.decode('utf-8'), file_id, mode_kind(newmode)))
578
def changes_from_git_changes(changes, mapping, specific_files=None, include_unchanged=False,
580
"""Create a iter_changes-like generator from a git stream.
582
source and target are iterators over tuples with:
583
(filename, sha, mode)
585
if target_extras is None:
586
target_extras = set()
587
for (oldpath, newpath), (oldmode, newmode), (oldsha, newsha) in changes:
588
if not (specific_files is None or
589
(oldpath is not None and osutils.is_inside_or_parent_of_any(specific_files, oldpath)) or
590
(newpath is not None and osutils.is_inside_or_parent_of_any(specific_files, newpath))):
592
path = (oldpath, newpath)
593
if oldpath is not None and mapping.is_special_file(oldpath):
595
if newpath is not None and mapping.is_special_file(newpath):
598
fileid = mapping.generate_file_id(newpath)
606
oldpath = oldpath.decode("utf-8")
608
oldexe = mode_is_executable(oldmode)
609
oldkind = mode_kind(oldmode)
617
(oldparentpath, oldname) = osutils.split(oldpath)
618
oldparent = mapping.generate_file_id(oldparentpath)
619
fileid = mapping.generate_file_id(oldpath)
627
newversioned = (newpath not in target_extras)
629
newexe = mode_is_executable(newmode)
630
newkind = mode_kind(newmode)
634
newpath = newpath.decode("utf-8")
639
newparentpath, newname = osutils.split(newpath)
640
newparent = mapping.generate_file_id(newparentpath)
641
if (not include_unchanged and
642
oldkind == 'directory' and newkind == 'directory' and
645
yield (fileid, (oldpath, newpath), (oldsha != newsha),
646
(oldversioned, newversioned),
647
(oldparent, newparent), (oldname, newname),
648
(oldkind, newkind), (oldexe, newexe))
651
class InterGitTrees(_mod_tree.InterTree):
652
"""InterTree that works between two git trees."""
654
_matching_from_tree_format = None
655
_matching_to_tree_format = None
656
_test_mutable_trees_to_test_trees = None
659
def is_compatible(cls, source, target):
660
return (isinstance(source, GitRevisionTree) and
661
isinstance(target, GitRevisionTree))
663
def compare(self, want_unchanged=False, specific_files=None,
664
extra_trees=None, require_versioned=False, include_root=False,
665
want_unversioned=False):
666
with self.lock_read():
667
changes, target_extras = self._iter_git_changes(
668
want_unchanged=want_unchanged,
669
require_versioned=require_versioned,
670
specific_files=specific_files,
671
extra_trees=extra_trees,
672
want_unversioned=want_unversioned)
673
source_fileid_map = self.source._fileid_map
674
target_fileid_map = self.target._fileid_map
675
return tree_delta_from_git_changes(changes, self.target.mapping,
676
(source_fileid_map, target_fileid_map),
677
specific_files=specific_files, include_root=include_root,
678
target_extras=target_extras)
680
def iter_changes(self, include_unchanged=False, specific_files=None,
681
pb=None, extra_trees=[], require_versioned=True,
682
want_unversioned=False):
683
with self.lock_read():
684
changes, target_extras = self._iter_git_changes(
685
want_unchanged=include_unchanged,
686
require_versioned=require_versioned,
687
specific_files=specific_files,
688
extra_trees=extra_trees,
689
want_unversioned=want_unversioned)
690
return changes_from_git_changes(
691
changes, self.target.mapping,
692
specific_files=specific_files,
693
include_unchanged=include_unchanged,
694
target_extras=target_extras)
696
def _iter_git_changes(self, want_unchanged=False, specific_files=None,
697
require_versioned=False, extra_trees=None,
698
want_unversioned=False):
699
raise NotImplementedError(self._iter_git_changes)
702
class InterGitRevisionTrees(InterGitTrees):
703
"""InterTree that works between two git revision trees."""
705
_matching_from_tree_format = None
706
_matching_to_tree_format = None
707
_test_mutable_trees_to_test_trees = None
710
def is_compatible(cls, source, target):
711
return (isinstance(source, GitRevisionTree) and
712
isinstance(target, GitRevisionTree))
714
def _iter_git_changes(self, want_unchanged=False, specific_files=None,
715
require_versioned=True, extra_trees=None,
716
want_unversioned=False):
717
trees = [self.source]
718
if extra_trees is not None:
719
trees.extend(extra_trees)
720
if specific_files is not None:
721
specific_files = self.target.find_related_paths_across_trees(
722
specific_files, trees,
723
require_versioned=require_versioned)
725
if self.source._repository._git.object_store != self.target._repository._git.object_store:
726
store = OverlayObjectStore([self.source._repository._git.object_store,
727
self.target._repository._git.object_store])
729
store = self.source._repository._git.object_store
730
return self.source._repository._git.object_store.tree_changes(
731
self.source.tree, self.target.tree, want_unchanged=want_unchanged,
732
include_trees=True, change_type_same=True), set()
735
_mod_tree.InterTree.register_optimiser(InterGitRevisionTrees)
738
class MutableGitIndexTree(mutabletree.MutableTree):
741
self._lock_mode = None
743
self._versioned_dirs = None
745
def is_versioned(self, path):
746
with self.lock_read():
747
path = path.rstrip('/').encode('utf-8')
748
return (path in self.index or self._has_dir(path))
750
def _has_dir(self, path):
753
if self._versioned_dirs is None:
755
return path in self._versioned_dirs
757
def _load_dirs(self):
758
if self._lock_mode is None:
759
raise errors.ObjectNotLocked(self)
760
self._versioned_dirs = set()
762
self._ensure_versioned_dir(posixpath.dirname(p))
764
def _ensure_versioned_dir(self, dirname):
765
if dirname in self._versioned_dirs:
768
self._ensure_versioned_dir(posixpath.dirname(dirname))
769
self._versioned_dirs.add(dirname)
771
def path2id(self, path):
772
with self.lock_read():
773
path = path.rstrip('/')
774
if self.is_versioned(path.rstrip('/')):
775
return self._fileid_map.lookup_file_id(path.encode("utf-8"))
778
def has_id(self, file_id):
780
self.id2path(file_id)
781
except errors.NoSuchId:
786
def id2path(self, file_id):
789
if type(file_id) is not bytes:
790
raise TypeError(file_id)
791
with self.lock_read():
793
path = self._fileid_map.lookup_path(file_id)
795
raise errors.NoSuchId(self, file_id)
796
path = path.decode('utf-8')
797
if self.is_versioned(path):
799
raise errors.NoSuchId(self, file_id)
801
def _set_root_id(self, file_id):
802
self._fileid_map.set_file_id("", file_id)
804
def get_root_id(self):
805
return self.path2id("")
807
def _add(self, files, ids, kinds):
808
for (path, file_id, kind) in zip(files, ids, kinds):
809
if file_id is not None:
810
raise workingtree.SettingFileIdUnsupported()
811
path, can_access = osutils.normalized_filename(path)
813
raise errors.InvalidNormalization(path)
814
self._index_add_entry(path, kind)
816
def _index_add_entry(self, path, kind, flags=0):
817
if not isinstance(path, basestring):
818
raise TypeError(path)
819
if kind == "directory":
820
# Git indexes don't contain directories
825
file, stat_val = self.get_file_with_stat(path)
826
except (errors.NoSuchFile, IOError):
827
# TODO: Rather than come up with something here, use the old index
829
stat_val = os.stat_result(
830
(stat.S_IFREG | 0644, 0, 0, 0, 0, 0, 0, 0, 0, 0))
831
blob.set_raw_string(file.read())
832
elif kind == "symlink":
835
stat_val = self._lstat(path)
836
except (errors.NoSuchFile, OSError):
837
# TODO: Rather than come up with something here, use the
839
stat_val = os.stat_result(
840
(stat.S_IFLNK, 0, 0, 0, 0, 0, 0, 0, 0, 0))
842
self.get_symlink_target(path).encode("utf-8"))
844
raise AssertionError("unknown kind '%s'" % kind)
845
# Add object to the repository if it didn't exist yet
846
if not blob.id in self.store:
847
self.store.add_object(blob)
848
# Add an entry to the index or update the existing entry
849
ensure_normalized_path(path)
850
encoded_path = path.encode("utf-8")
851
if b'\r' in encoded_path or b'\n' in encoded_path:
852
# TODO(jelmer): Why do we need to do this?
853
trace.mutter('ignoring path with invalid newline in it: %r', path)
855
self.index[encoded_path] = index_entry_from_stat(
856
stat_val, blob.id, flags)
857
if self._versioned_dirs is not None:
858
self._ensure_versioned_dir(encoded_path)
860
def iter_entries_by_dir(self, specific_files=None, yield_parents=False):
862
raise NotImplementedError(self.iter_entries_by_dir)
863
with self.lock_read():
864
if specific_files is not None:
865
specific_files = set(specific_files)
867
specific_files = None
868
root_ie = self._get_dir_ie(u"", None)
870
if specific_files is None or u"" in specific_files:
871
ret[(None, u"")] = root_ie
872
dir_ids = {u"": root_ie.file_id}
873
for path, value in self.index.iteritems():
874
if self.mapping.is_special_file(path):
876
path = path.decode("utf-8")
877
if specific_files is not None and not path in specific_files:
879
(parent, name) = posixpath.split(path)
881
file_ie = self._get_file_ie(name, path, value, None)
882
except errors.NoSuchFile:
884
if yield_parents or specific_files is None:
885
for (dir_path, dir_ie) in self._add_missing_parent_ids(parent,
887
ret[(posixpath.dirname(dir_path), dir_path)] = dir_ie
888
file_ie.parent_id = self.path2id(parent)
889
ret[(posixpath.dirname(path), path)] = file_ie
890
return ((path, ie) for ((_, path), ie) in sorted(ret.items()))
893
def _get_dir_ie(self, path, parent_id):
894
file_id = self.path2id(path)
895
return GitTreeDirectory(file_id,
896
posixpath.basename(path).strip("/"), parent_id)
898
def _get_file_ie(self, name, path, value, parent_id):
899
if type(name) is not unicode:
900
raise TypeError(name)
901
if type(path) is not unicode:
902
raise TypeError(path)
903
if not isinstance(value, tuple) or len(value) != 10:
904
raise TypeError(value)
905
(ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = value
906
file_id = self.path2id(path)
907
if type(file_id) != str:
909
kind = mode_kind(mode)
910
ie = entry_factory[kind](file_id, name, parent_id)
911
if kind == 'symlink':
912
ie.symlink_target = self.get_symlink_target(path, file_id)
915
data = self.get_file_text(path, file_id)
916
except errors.NoSuchFile:
919
if e.errno != errno.ENOENT:
923
data = self.branch.repository._git.object_store[sha].data
924
ie.text_sha1 = osutils.sha_string(data)
925
ie.text_size = len(data)
926
ie.executable = bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
929
def _add_missing_parent_ids(self, path, dir_ids):
932
parent = posixpath.dirname(path).strip("/")
933
ret = self._add_missing_parent_ids(parent, dir_ids)
934
parent_id = dir_ids[parent]
935
ie = self._get_dir_ie(path, parent_id)
936
dir_ids[path] = ie.file_id
937
ret.append((path, ie))
940
def _comparison_data(self, entry, path):
942
return None, False, None
943
return entry.kind, entry.executable, None
945
def _unversion_path(self, path):
946
if self._lock_mode is None:
947
raise errors.ObjectNotLocked(self)
948
encoded_path = path.encode("utf-8")
951
del self.index[encoded_path]
953
# A directory, perhaps?
954
for p in list(self.index):
955
if p.startswith(encoded_path+b"/"):
960
self._versioned_dirs = None
963
def unversion(self, paths, file_ids=None):
964
with self.lock_tree_write():
966
if self._unversion_path(path) == 0:
967
raise errors.NoSuchFile(path)
968
self._versioned_dirs = None
974
def update_basis_by_delta(self, revid, delta):
975
# TODO(jelmer): This shouldn't be called, it's inventory specific.
976
for (old_path, new_path, file_id, ie) in delta:
977
if old_path is not None and old_path.encode('utf-8') in self.index:
978
del self.index[old_path.encode('utf-8')]
979
self._versioned_dirs = None
980
if new_path is not None and ie.kind != 'directory':
981
self._index_add_entry(new_path, ie.kind)
983
self._set_merges_from_parent_ids([])
985
def move(self, from_paths, to_dir=None, after=None):
987
with self.lock_tree_write():
988
to_abs = self.abspath(to_dir)
989
if not os.path.isdir(to_abs):
990
raise errors.BzrMoveFailedError('', to_dir,
991
errors.NotADirectory(to_abs))
993
for from_rel in from_paths:
994
from_tail = os.path.split(from_rel)[-1]
995
to_rel = os.path.join(to_dir, from_tail)
996
self.rename_one(from_rel, to_rel, after=after)
997
rename_tuples.append((from_rel, to_rel))
1001
def rename_one(self, from_rel, to_rel, after=None):
1002
from_path = from_rel.encode("utf-8")
1003
to_rel, can_access = osutils.normalized_filename(to_rel)
1005
raise errors.InvalidNormalization(to_rel)
1006
to_path = to_rel.encode("utf-8")
1007
with self.lock_tree_write():
1009
# Perhaps it's already moved?
1011
not self.has_filename(from_rel) and
1012
self.has_filename(to_rel) and
1013
not self.is_versioned(to_rel))
1015
if not self.has_filename(to_rel):
1016
raise errors.BzrMoveFailedError(from_rel, to_rel,
1017
errors.NoSuchFile(to_rel))
1018
if self.basis_tree().is_versioned(to_rel):
1019
raise errors.BzrMoveFailedError(from_rel, to_rel,
1020
errors.AlreadyVersionedError(to_rel))
1022
kind = self.kind(to_rel)
1025
to_kind = self.kind(to_rel)
1026
except errors.NoSuchFile:
1027
exc_type = errors.BzrRenameFailedError
1030
exc_type = errors.BzrMoveFailedError
1031
if self.is_versioned(to_rel):
1032
raise exc_type(from_rel, to_rel,
1033
errors.AlreadyVersionedError(to_rel))
1034
if not self.has_filename(from_rel):
1035
raise errors.BzrMoveFailedError(from_rel, to_rel,
1036
errors.NoSuchFile(from_rel))
1037
kind = self.kind(from_rel)
1038
if not self.is_versioned(from_rel) and kind != 'directory':
1039
raise exc_type(from_rel, to_rel,
1040
errors.NotVersionedError(from_rel))
1041
if self.has_filename(to_rel):
1042
raise errors.RenameFailedFilesExist(
1043
from_rel, to_rel, errors.FileExists(to_rel))
1045
kind = self.kind(from_rel)
1047
if not after and not from_path in self.index and kind != 'directory':
1049
raise errors.BzrMoveFailedError(from_rel, to_rel,
1050
errors.NotVersionedError(path=from_rel))
1054
self._rename_one(from_rel, to_rel)
1055
except OSError as e:
1056
if e.errno == errno.ENOENT:
1057
raise errors.BzrMoveFailedError(from_rel, to_rel,
1058
errors.NoSuchFile(to_rel))
1060
if kind != 'directory':
1062
del self.index[from_path]
1065
self._index_add_entry(to_rel, kind)
1067
todo = [p for p in self.index if p.startswith(from_path+'/')]
1069
self.index[posixpath.join(to_path, posixpath.relpath(p, from_path))] = self.index[p]
1072
self._versioned_dirs = None
1075
def find_related_paths_across_trees(self, paths, trees=[],
1076
require_versioned=True):
1080
if require_versioned:
1081
trees = [self] + (trees if trees is not None else [])
1085
if t.is_versioned(p):
1090
raise errors.PathsNotVersionedError(unversioned)
1092
return filter(self.is_versioned, paths)