1
# Copyright (C) 2008-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
18
"""An adapter between a Git index and a Bazaar Working Tree"""
20
from __future__ import absolute_import
23
from collections import defaultdict
25
from dulwich.ignore import (
28
from dulwich.file import GitFile, FileLocked
29
from dulwich.index import (
32
build_index_from_tree,
33
index_entry_from_path,
34
index_entry_from_stat,
40
from dulwich.object_store import (
43
from dulwich.objects import (
52
conflicts as _mod_conflicts,
54
controldir as _mod_controldir,
60
revision as _mod_revision,
62
transport as _mod_transport,
66
from ..decorators import (
69
from ..mutabletree import (
81
from .mapping import (
87
class GitWorkingTree(MutableGitIndexTree, workingtree.WorkingTree):
88
"""A Git working tree."""
90
def __init__(self, controldir, repo, branch):
91
MutableGitIndexTree.__init__(self)
92
basedir = controldir.root_transport.local_abspath('.')
93
self.basedir = osutils.realpath(basedir)
94
self.controldir = controldir
95
self.repository = repo
96
self.store = self.repository._git.object_store
97
self.mapping = self.repository.get_mapping()
99
self._transport = self.repository._git._controltransport
100
self._format = GitWorkingTreeFormat()
102
self._index_file = None
103
self.views = self._make_views()
104
self._rules_searcher = None
105
self._detect_case_handling()
108
def supports_tree_reference(self):
111
def supports_rename_tracking(self):
114
def _read_index(self):
115
self.index = Index(self.control_transport.local_abspath('index'))
116
self._index_dirty = False
119
"""Lock the repository for read operations.
121
:return: A breezy.lock.LogicalLockResult.
123
if not self._lock_mode:
124
self._lock_mode = 'r'
128
self._lock_count += 1
129
self.branch.lock_read()
130
return lock.LogicalLockResult(self.unlock)
132
def _lock_write_tree(self):
133
if not self._lock_mode:
134
self._lock_mode = 'w'
137
self._index_file = GitFile(
138
self.control_transport.local_abspath('index'), 'wb')
140
raise errors.LockContention('index')
142
elif self._lock_mode == 'r':
143
raise errors.ReadOnlyError(self)
145
self._lock_count += 1
147
def lock_tree_write(self):
148
self.branch.lock_read()
150
self._lock_write_tree()
151
return lock.LogicalLockResult(self.unlock)
152
except BaseException:
156
def lock_write(self, token=None):
157
self.branch.lock_write()
159
self._lock_write_tree()
160
return lock.LogicalLockResult(self.unlock)
161
except BaseException:
166
return self._lock_count >= 1
168
def get_physical_lock_status(self):
171
def break_lock(self):
173
self.control_transport.delete('index.lock')
174
except errors.NoSuchFile:
176
self.branch.break_lock()
178
@only_raises(errors.LockNotHeld, errors.LockBroken)
180
if not self._lock_count:
181
return lock.cant_unlock_not_held(self)
184
self._lock_count -= 1
185
if self._lock_count > 0:
187
if self._index_file is not None:
188
if self._index_dirty:
189
self._flush(self._index_file)
190
self._index_file.close()
192
# Something else already triggered a write of the index
193
# file by calling .flush()
194
self._index_file.abort()
195
self._index_file = None
196
self._lock_mode = None
204
def _detect_case_handling(self):
206
self._transport.stat(".git/cOnFiG")
207
except errors.NoSuchFile:
208
self.case_sensitive = True
210
self.case_sensitive = False
212
def get_transform(self, pb=None):
213
from ..transform import TreeTransform
214
return TreeTransform(self, pb=pb)
216
def merge_modified(self):
219
def set_merge_modified(self, modified_hashes):
220
raise errors.UnsupportedOperation(self.set_merge_modified, self)
222
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
223
self.set_parent_ids([p for p, t in parents_list])
225
def _set_merges_from_parent_ids(self, rhs_parent_ids):
227
merges = [self.branch.lookup_bzr_revision_id(
228
revid)[0] for revid in rhs_parent_ids]
229
except errors.NoSuchRevision as e:
230
raise errors.GhostRevisionUnusableHere(e.revision)
232
self.control_transport.put_bytes(
233
'MERGE_HEAD', b'\n'.join(merges),
234
mode=self.controldir._get_file_mode())
237
self.control_transport.delete('MERGE_HEAD')
238
except errors.NoSuchFile:
241
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
242
"""Set the parent ids to revision_ids.
244
See also set_parent_trees. This api will try to retrieve the tree data
245
for each element of revision_ids from the trees repository. If you have
246
tree data already available, it is more efficient to use
247
set_parent_trees rather than set_parent_ids. set_parent_ids is however
248
an easier API to use.
250
:param revision_ids: The revision_ids to set as the parent ids of this
251
working tree. Any of these may be ghosts.
253
with self.lock_tree_write():
254
self._check_parents_for_ghosts(
255
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
256
for revision_id in revision_ids:
257
_mod_revision.check_not_reserved_id(revision_id)
259
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
261
if len(revision_ids) > 0:
262
self.set_last_revision(revision_ids[0])
264
self.set_last_revision(_mod_revision.NULL_REVISION)
266
self._set_merges_from_parent_ids(revision_ids[1:])
268
def get_parent_ids(self):
269
"""See Tree.get_parent_ids.
271
This implementation reads the pending merges list and last_revision
272
value and uses that to decide what the parents list should be.
274
last_rev = _mod_revision.ensure_null(self._last_revision())
275
if _mod_revision.NULL_REVISION == last_rev:
280
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
281
except errors.NoSuchFile:
284
for l in osutils.split_lines(merges_bytes):
285
revision_id = l.rstrip(b'\n')
287
self.branch.lookup_foreign_revision_id(revision_id))
290
def check_state(self):
291
"""Check that the working state is/isn't valid."""
294
def remove(self, files, verbose=False, to_file=None, keep_files=True,
296
"""Remove nominated files from the working tree metadata.
298
:param files: File paths relative to the basedir.
299
:param keep_files: If true, the files will also be kept.
300
:param force: Delete files and directories, even if they are changed
301
and even if the directories are not empty.
303
if not isinstance(files, list):
309
def backup(file_to_backup):
310
abs_path = self.abspath(file_to_backup)
311
backup_name = self.controldir._available_backup_name(
313
osutils.rename(abs_path, self.abspath(backup_name))
314
return "removed %s (but kept a copy: %s)" % (
315
file_to_backup, backup_name)
317
# Sort needed to first handle directory content before the directory
322
def recurse_directory_to_add_files(directory):
323
# Recurse directory and add all files
324
# so we can check if they have changed.
325
for parent_info, file_infos in self.walkdirs(directory):
326
for relpath, basename, kind, lstat, fileid, kind in file_infos:
327
# Is it versioned or ignored?
328
if self.is_versioned(relpath):
329
# Add nested content for deletion.
330
all_files.add(relpath)
332
# Files which are not versioned
333
# should be treated as unknown.
334
files_to_backup.append(relpath)
336
with self.lock_tree_write():
337
for filepath in files:
338
# Get file name into canonical form.
339
abspath = self.abspath(filepath)
340
filepath = self.relpath(abspath)
343
all_files.add(filepath)
344
recurse_directory_to_add_files(filepath)
346
files = list(all_files)
349
return # nothing to do
351
# Sort needed to first handle directory content before the
353
files.sort(reverse=True)
355
# Bail out if we are going to delete files we shouldn't
356
if not keep_files and not force:
357
for change in self.iter_changes(
358
self.basis_tree(), include_unchanged=True,
359
require_versioned=False, want_unversioned=True,
360
specific_files=files):
361
if change.versioned[0] is False:
362
# The record is unknown or newly added
363
files_to_backup.append(change.path[1])
364
files_to_backup.extend(
365
osutils.parent_directories(change.path[1]))
366
elif (change.changed_content and (change.kind[1] is not None)
367
and osutils.is_inside_any(files, change.path[1])):
368
# Versioned and changed, but not deleted, and still
369
# in one of the dirs to be deleted.
370
files_to_backup.append(change.path[1])
371
files_to_backup.extend(
372
osutils.parent_directories(change.path[1]))
380
except errors.NoSuchFile:
383
abs_path = self.abspath(f)
385
# having removed it, it must be either ignored or unknown
386
if self.is_ignored(f):
390
kind_ch = osutils.kind_marker(kind)
391
to_file.write(new_status + ' ' + f + kind_ch + '\n')
393
message = "%s does not exist" % (f, )
396
if f in files_to_backup and not force:
399
if kind == 'directory':
400
osutils.rmtree(abs_path)
402
osutils.delete_any(abs_path)
403
message = "deleted %s" % (f,)
405
message = "removed %s" % (f,)
406
self._unversion_path(f)
408
# print only one message (if any) per file.
409
if message is not None:
411
self._versioned_dirs = None
413
def smart_add(self, file_list, recurse=True, action=None, save=True):
417
# expand any symlinks in the directory part, while leaving the
419
# only expanding if symlinks are supported avoids windows path bugs
420
if self.supports_symlinks():
421
file_list = list(map(osutils.normalizepath, file_list))
423
conflicts_related = set()
424
for c in self.conflicts():
425
conflicts_related.update(c.associated_filenames())
431
def call_action(filepath, kind):
434
if action is not None:
435
parent_path = posixpath.dirname(filepath)
436
parent_id = self.path2id(parent_path)
437
parent_ie = self._get_dir_ie(parent_path, parent_id)
438
file_id = action(self, parent_ie, filepath, kind)
439
if file_id is not None:
440
raise workingtree.SettingFileIdUnsupported()
442
with self.lock_tree_write():
443
for filepath in osutils.canonical_relpaths(
444
self.basedir, file_list):
445
filepath, can_access = osutils.normalized_filename(filepath)
447
raise errors.InvalidNormalization(filepath)
449
abspath = self.abspath(filepath)
450
kind = osutils.file_kind(abspath)
451
if kind in ("file", "symlink"):
452
(index, subpath) = self._lookup_index(
453
filepath.encode('utf-8'))
457
call_action(filepath, kind)
459
self._index_add_entry(filepath, kind)
460
added.append(filepath)
461
elif kind == "directory":
462
(index, subpath) = self._lookup_index(
463
filepath.encode('utf-8'))
464
if subpath not in index:
465
call_action(filepath, kind)
467
user_dirs.append(filepath)
469
raise errors.BadFileKindError(filename=abspath, kind=kind)
470
for user_dir in user_dirs:
471
abs_user_dir = self.abspath(user_dir)
474
transport = _mod_transport.get_transport_from_path(
476
_mod_controldir.ControlDirFormat.find_format(transport)
478
except errors.NotBranchError:
480
except errors.UnsupportedFormatError:
485
trace.warning('skipping nested tree %r', abs_user_dir)
488
for name in os.listdir(abs_user_dir):
489
subp = os.path.join(user_dir, name)
490
if (self.is_control_filename(subp) or
491
self.mapping.is_special_file(subp)):
493
ignore_glob = self.is_ignored(subp)
494
if ignore_glob is not None:
495
ignored.setdefault(ignore_glob, []).append(subp)
497
abspath = self.abspath(subp)
498
kind = osutils.file_kind(abspath)
499
if kind == "directory":
500
user_dirs.append(subp)
502
(index, subpath) = self._lookup_index(
503
subp.encode('utf-8'))
507
if subp in conflicts_related:
509
call_action(subp, kind)
511
self._index_add_entry(subp, kind)
513
return added, ignored
515
def has_filename(self, filename):
516
return osutils.lexists(self.abspath(filename))
518
def _iter_files_recursive(self, from_dir=None, include_dirs=False):
521
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
522
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
523
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
524
if self.controldir.is_control_filename(
525
dir_relpath.decode(osutils._fs_enc)):
527
for name in list(dirnames):
528
if self.controldir.is_control_filename(
529
name.decode(osutils._fs_enc)):
530
dirnames.remove(name)
532
relpath = os.path.join(dir_relpath, name)
535
yield relpath.decode(osutils._fs_enc)
536
except UnicodeDecodeError:
537
raise errors.BadFilenameEncoding(
538
relpath, osutils._fs_enc)
539
if not self._has_dir(relpath):
540
dirnames.remove(name)
541
for name in filenames:
542
if self.mapping.is_special_file(name):
544
if self.controldir.is_control_filename(
545
name.decode(osutils._fs_enc, 'replace')):
547
yp = os.path.join(dir_relpath, name)
549
yield yp.decode(osutils._fs_enc)
550
except UnicodeDecodeError:
551
raise errors.BadFilenameEncoding(
555
"""Yield all unversioned files in this WorkingTree.
557
with self.lock_read():
559
[p.decode('utf-8') for p, i in self._recurse_index_entries()])
560
all_paths = set(self._iter_files_recursive(include_dirs=False))
561
return iter(all_paths - index_paths)
563
def _gather_kinds(self, files, kinds):
564
"""See MutableTree._gather_kinds."""
565
with self.lock_tree_write():
566
for pos, f in enumerate(files):
567
if kinds[pos] is None:
568
fullpath = osutils.normpath(self.abspath(f))
570
kind = osutils.file_kind(fullpath)
572
if e.errno == errno.ENOENT:
573
raise errors.NoSuchFile(fullpath)
574
if f != '' and self._directory_is_tree_reference(f):
575
kind = 'tree-reference'
579
if self._lock_mode != 'w':
580
raise errors.NotWriteLocked(self)
581
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
582
# already in use and GitFile doesn't allow overriding the lock file
584
f = open(self.control_transport.local_abspath('index'), 'wb')
585
# Note that _flush will close the file
591
write_index_dict(shaf, self.index)
593
except BaseException:
596
self._index_dirty = False
598
def has_or_had_id(self, file_id):
599
if self.has_id(file_id):
601
if self.had_id(file_id):
605
def had_id(self, file_id):
606
path = self._basis_fileid_map.lookup_path(file_id)
608
head = self.repository._git.head()
610
# Assume no if basis is not accessible
613
root_tree = self.store[head].tree
617
tree_lookup_path(self.store.__getitem__,
618
root_tree, path.encode('utf-8'))
624
def get_file_mtime(self, path):
625
"""See Tree.get_file_mtime."""
627
return self._lstat(path).st_mtime
629
if e.errno == errno.ENOENT:
630
raise errors.NoSuchFile(path)
633
def is_ignored(self, filename):
634
r"""Check whether the filename matches an ignore pattern.
636
If the file is ignored, returns the pattern which caused it to
637
be ignored, otherwise None. So this can simply be used as a
638
boolean if desired."""
639
if getattr(self, '_global_ignoreglobster', None) is None:
641
ignore_globs.update(ignores.get_runtime_ignores())
642
ignore_globs.update(ignores.get_user_ignores())
643
self._global_ignoreglobster = globbing.ExceptionGlobster(
645
match = self._global_ignoreglobster.match(filename)
646
if match is not None:
649
if self.kind(filename) == 'directory':
651
except errors.NoSuchFile:
653
filename = filename.lstrip('/')
654
ignore_manager = self._get_ignore_manager()
655
ps = list(ignore_manager.find_matching(filename))
658
if not ps[-1].is_exclude:
662
def _get_ignore_manager(self):
663
ignoremanager = getattr(self, '_ignoremanager', None)
664
if ignoremanager is not None:
667
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
668
self._ignoremanager = ignore_manager
669
return ignore_manager
671
def _flush_ignore_list_cache(self):
672
self._ignoremanager = None
674
def set_last_revision(self, revid):
675
if _mod_revision.is_null(revid):
676
self.branch.set_last_revision_info(0, revid)
678
_mod_revision.check_not_reserved_id(revid)
680
self.branch.generate_revision_history(revid)
681
except errors.NoSuchRevision:
682
raise errors.GhostRevisionUnusableHere(revid)
684
def _reset_data(self):
686
head = self.repository._git.head()
688
self._basis_fileid_map = GitFileIdMap({}, self.mapping)
690
self._basis_fileid_map = self.mapping.get_fileid_map(
691
self.store.__getitem__, self.store[head].tree)
692
self._fileid_map = self._basis_fileid_map.copy()
694
def get_file_verifier(self, path, stat_value=None):
695
with self.lock_read():
696
(index, subpath) = self._lookup_index(path.encode('utf-8'))
698
return ("GIT", index[subpath].sha)
700
if self._has_dir(path):
702
raise errors.NoSuchFile(path)
704
def get_file_sha1(self, path, stat_value=None):
705
with self.lock_read():
706
if not self.is_versioned(path):
707
raise errors.NoSuchFile(path)
708
abspath = self.abspath(path)
710
return osutils.sha_file_by_name(abspath)
712
if e.errno in (errno.EISDIR, errno.ENOENT):
716
def revision_tree(self, revid):
717
return self.repository.revision_tree(revid)
719
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
720
mode = stat_result.st_mode
721
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
723
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
724
return self.basis_tree().is_executable(path)
726
def stored_kind(self, path):
727
with self.lock_read():
728
encoded_path = path.encode('utf-8')
729
(index, subpath) = self._lookup_index(encoded_path)
731
return mode_kind(index[subpath].mode)
733
# Maybe it's a directory?
734
if self._has_dir(encoded_path):
736
raise errors.NoSuchFile(path)
738
def _lstat(self, path):
739
return os.lstat(self.abspath(path))
741
def _live_entry(self, path):
742
encoded_path = self.abspath(path.decode('utf-8')).encode(
744
return index_entry_from_path(encoded_path)
746
def is_executable(self, path):
747
with self.lock_read():
748
if self._supports_executable():
749
mode = self._lstat(path).st_mode
751
(index, subpath) = self._lookup_index(path.encode('utf-8'))
753
mode = index[subpath].mode
756
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
758
def _is_executable_from_path_and_stat(self, path, stat_result):
759
if self._supports_executable():
760
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
762
return self._is_executable_from_path_and_stat_from_basis(
765
def list_files(self, include_root=False, from_dir=None, recursive=True):
766
if from_dir is None or from_dir == '.':
769
fk_entries = {'directory': tree.TreeDirectory,
770
'file': tree.TreeFile,
771
'symlink': tree.TreeLink,
772
'tree-reference': tree.TreeReference}
773
with self.lock_read():
774
root_ie = self._get_dir_ie(u"", None)
775
if include_root and not from_dir:
776
yield "", "V", root_ie.kind, root_ie
777
dir_ids[u""] = root_ie.file_id
779
path_iterator = sorted(
780
self._iter_files_recursive(from_dir, include_dirs=True))
782
encoded_from_dir = self.abspath(from_dir).encode(
784
path_iterator = sorted(
785
[os.path.join(from_dir, name.decode(osutils._fs_enc))
786
for name in os.listdir(encoded_from_dir)
787
if not self.controldir.is_control_filename(
788
name.decode(osutils._fs_enc)) and
789
not self.mapping.is_special_file(
790
name.decode(osutils._fs_enc))])
791
for path in path_iterator:
793
encoded_path = path.encode("utf-8")
794
except UnicodeEncodeError:
795
raise errors.BadFilenameEncoding(
796
path, osutils._fs_enc)
797
(index, index_path) = self._lookup_index(encoded_path)
799
value = index[index_path]
802
kind = self.kind(path)
803
parent, name = posixpath.split(path)
804
for dir_path, dir_ie in self._add_missing_parent_ids(
807
if kind in ('directory', 'tree-reference'):
809
if self._has_dir(encoded_path):
810
ie = self._get_dir_ie(path, self.path2id(path))
812
elif self.is_ignored(path):
814
ie = fk_entries[kind]()
817
ie = fk_entries[kind]()
818
yield (posixpath.relpath(path, from_dir), status, kind,
821
if value is not None:
822
ie = self._get_file_ie(name, path, value, dir_ids[parent])
823
yield (posixpath.relpath(path, from_dir), "V", ie.kind, ie)
825
ie = fk_entries[kind]()
826
yield (posixpath.relpath(path, from_dir),
827
("I" if self.is_ignored(path) else "?"), kind, ie)
829
def all_file_ids(self):
830
raise errors.UnsupportedOperation(self.all_file_ids, self)
832
def all_versioned_paths(self):
833
with self.lock_read():
835
for path in self.index:
836
if self.mapping.is_special_file(path):
838
path = path.decode("utf-8")
841
path = posixpath.dirname(path).strip("/")
847
def iter_child_entries(self, path):
848
encoded_path = path.encode('utf-8')
849
with self.lock_read():
850
parent_id = self.path2id(path)
852
for item_path, value in self.index.iteritems():
853
decoded_item_path = item_path.decode('utf-8')
854
if self.mapping.is_special_file(item_path):
856
if not osutils.is_inside(path, decoded_item_path):
859
subpath = posixpath.relpath(decoded_item_path, path)
861
dirname = subpath.split('/', 1)[0]
862
file_ie = self._get_dir_ie(
863
posixpath.join(path, dirname), parent_id)
865
(unused_parent, name) = posixpath.split(decoded_item_path)
866
file_ie = self._get_file_ie(
867
name, decoded_item_path, value, parent_id)
869
if not found_any and path != u'':
870
raise errors.NoSuchFile(path)
873
with self.lock_read():
874
conflicts = _mod_conflicts.ConflictList()
875
for item_path, value in self.index.iteritems():
876
if value.flags & FLAG_STAGEMASK:
877
conflicts.append(_mod_conflicts.TextConflict(
878
item_path.decode('utf-8')))
881
def set_conflicts(self, conflicts):
883
for conflict in conflicts:
884
if conflict.typestring in ('text conflict', 'contents conflict'):
885
by_path.add(conflict.path.encode('utf-8'))
887
raise errors.UnsupportedOperation(self.set_conflicts, self)
888
with self.lock_tree_write():
889
for path in self.index:
890
self._set_conflicted(path, path in by_path)
892
def _set_conflicted(self, path, conflicted):
893
trace.mutter('change conflict: %r -> %r', path, conflicted)
894
value = self.index[path]
895
self._index_dirty = True
897
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
899
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
901
def add_conflicts(self, new_conflicts):
902
with self.lock_tree_write():
903
for conflict in new_conflicts:
904
if conflict.typestring in ('text conflict',
905
'contents conflict'):
907
self._set_conflicted(
908
conflict.path.encode('utf-8'), True)
910
raise errors.UnsupportedOperation(
911
self.add_conflicts, self)
913
raise errors.UnsupportedOperation(self.add_conflicts, self)
915
def walkdirs(self, prefix=""):
916
"""Walk the directories of this tree.
918
returns a generator which yields items in the form:
919
((curren_directory_path, fileid),
920
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
923
This API returns a generator, which is only valid during the current
924
tree transaction - within a single lock_read or lock_write duration.
926
If the tree is not locked, it may cause an error to be raised,
927
depending on the tree implementation.
929
from bisect import bisect_left
931
disk_top = self.abspath(prefix)
932
if disk_top.endswith('/'):
933
disk_top = disk_top[:-1]
934
top_strip_len = len(disk_top) + 1
935
inventory_iterator = self._walkdirs(prefix)
936
disk_iterator = osutils.walkdirs(disk_top, prefix)
938
current_disk = next(disk_iterator)
939
disk_finished = False
941
if not (e.errno == errno.ENOENT
942
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
947
current_inv = next(inventory_iterator)
949
except StopIteration:
952
while not inv_finished or not disk_finished:
954
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
955
cur_disk_dir_content) = current_disk
957
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
958
cur_disk_dir_content) = ((None, None), None)
959
if not disk_finished:
960
# strip out .bzr dirs
961
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
962
and len(cur_disk_dir_content) > 0):
963
# osutils.walkdirs can be made nicer -
964
# yield the path-from-prefix rather than the pathjoined
966
bzrdir_loc = bisect_left(cur_disk_dir_content,
968
if (bzrdir_loc < len(cur_disk_dir_content) and
969
self.controldir.is_control_filename(
970
cur_disk_dir_content[bzrdir_loc][0])):
971
# we dont yield the contents of, or, .bzr itself.
972
del cur_disk_dir_content[bzrdir_loc]
974
# everything is unknown
977
# everything is missing
980
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
981
- (current_inv[0][0] < cur_disk_dir_relpath))
983
# disk is before inventory - unknown
984
dirblock = [(relpath, basename, kind, stat, None, None) for
985
relpath, basename, kind, stat, top_path in
986
cur_disk_dir_content]
987
yield (cur_disk_dir_relpath, None), dirblock
989
current_disk = next(disk_iterator)
990
except StopIteration:
993
# inventory is before disk - missing.
994
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
995
for relpath, basename, dkind, stat, fileid, kind in
997
yield (current_inv[0][0], current_inv[0][1]), dirblock
999
current_inv = next(inventory_iterator)
1000
except StopIteration:
1003
# versioned present directory
1004
# merge the inventory and disk data together
1006
for relpath, subiterator in itertools.groupby(sorted(
1007
current_inv[1] + cur_disk_dir_content,
1008
key=operator.itemgetter(0)), operator.itemgetter(1)):
1009
path_elements = list(subiterator)
1010
if len(path_elements) == 2:
1011
inv_row, disk_row = path_elements
1012
# versioned, present file
1013
dirblock.append((inv_row[0],
1014
inv_row[1], disk_row[2],
1015
disk_row[3], inv_row[4],
1017
elif len(path_elements[0]) == 5:
1020
(path_elements[0][0], path_elements[0][1],
1021
path_elements[0][2], path_elements[0][3],
1023
elif len(path_elements[0]) == 6:
1024
# versioned, absent file.
1026
(path_elements[0][0], path_elements[0][1],
1027
'unknown', None, path_elements[0][4],
1028
path_elements[0][5]))
1030
raise NotImplementedError('unreachable code')
1031
yield current_inv[0], dirblock
1033
current_inv = next(inventory_iterator)
1034
except StopIteration:
1037
current_disk = next(disk_iterator)
1038
except StopIteration:
1039
disk_finished = True
1041
def _walkdirs(self, prefix=u""):
1044
prefix = prefix.encode('utf-8')
1045
per_dir = defaultdict(set)
1047
per_dir[(u'', self.get_root_id())] = set()
1049
def add_entry(path, kind):
1050
if path == b'' or not path.startswith(prefix):
1052
(dirname, child_name) = posixpath.split(path)
1053
add_entry(dirname, 'directory')
1054
dirname = dirname.decode("utf-8")
1055
dir_file_id = self.path2id(dirname)
1056
if not isinstance(value, tuple) or len(value) != 10:
1057
raise ValueError(value)
1058
per_dir[(dirname, dir_file_id)].add(
1059
(path.decode("utf-8"), child_name.decode("utf-8"),
1061
self.path2id(path.decode("utf-8")),
1063
with self.lock_read():
1064
for path, value in self.index.iteritems():
1065
if self.mapping.is_special_file(path):
1067
if not path.startswith(prefix):
1069
add_entry(path, mode_kind(value.mode))
1070
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1072
def get_shelf_manager(self):
1073
raise workingtree.ShelvingUnsupported()
1075
def store_uncommitted(self):
1076
raise errors.StoringUncommittedNotSupported(self)
1078
def apply_inventory_delta(self, changes):
1079
for (old_path, new_path, file_id, ie) in changes:
1080
if old_path is not None:
1081
(index, old_subpath) = self._lookup_index(
1082
old_path.encode('utf-8'))
1084
self._index_del_entry(index, old_subpath)
1088
self._versioned_dirs = None
1089
if new_path is not None and ie.kind != 'directory':
1090
if ie.kind == 'tree-reference':
1091
self._index_add_entry(
1093
reference_revision=ie.reference_revision)
1095
self._index_add_entry(new_path, ie.kind)
1098
def annotate_iter(self, path,
1099
default_revision=_mod_revision.CURRENT_REVISION):
1100
"""See Tree.annotate_iter
1102
This implementation will use the basis tree implementation if possible.
1103
Lines not in the basis are attributed to CURRENT_REVISION
1105
If there are pending merges, lines added by those merges will be
1106
incorrectly attributed to CURRENT_REVISION (but after committing, the
1107
attribution will be correct).
1109
with self.lock_read():
1110
maybe_file_parent_keys = []
1111
for parent_id in self.get_parent_ids():
1113
parent_tree = self.revision_tree(parent_id)
1114
except errors.NoSuchRevisionInTree:
1115
parent_tree = self.branch.repository.revision_tree(
1117
with parent_tree.lock_read():
1118
# TODO(jelmer): Use rename/copy tracker to find path name
1122
kind = parent_tree.kind(parent_path)
1123
except errors.NoSuchFile:
1126
# Note: this is slightly unnecessary, because symlinks
1127
# and directories have a "text" which is the empty
1128
# text, and we know that won't mess up annotations. But
1133
parent_tree.get_file_revision(parent_path))
1134
if parent_text_key not in maybe_file_parent_keys:
1135
maybe_file_parent_keys.append(parent_text_key)
1136
# Now we have the parents of this content
1137
from breezy.annotate import Annotator
1138
from .annotate import AnnotateProvider
1139
annotate_provider = AnnotateProvider(
1140
self.branch.repository._file_change_scanner)
1141
annotator = Annotator(annotate_provider)
1143
from breezy.graph import Graph
1144
graph = Graph(annotate_provider)
1145
heads = graph.heads(maybe_file_parent_keys)
1146
file_parent_keys = []
1147
for key in maybe_file_parent_keys:
1149
file_parent_keys.append(key)
1151
text = self.get_file_text(path)
1152
this_key = (path, default_revision)
1153
annotator.add_special_text(this_key, file_parent_keys, text)
1154
annotations = [(key[-1], line)
1155
for key, line in annotator.annotate_flat(this_key)]
1158
def _rename_one(self, from_rel, to_rel):
1159
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1161
def _build_checkout_with_index(self):
1162
build_index_from_tree(
1163
self.user_transport.local_abspath('.'),
1164
self.control_transport.local_abspath("index"),
1167
if self.branch.head is None
1168
else self.store[self.branch.head].tree,
1169
honor_filemode=self._supports_executable())
1171
def reset_state(self, revision_ids=None):
1172
"""Reset the state of the working tree.
1174
This does a hard-reset to a last-known-good state. This is a way to
1175
fix if something got corrupted (like the .git/index file)
1177
with self.lock_tree_write():
1178
if revision_ids is not None:
1179
self.set_parent_ids(revision_ids)
1181
self._index_dirty = True
1182
if self.branch.head is not None:
1183
for entry in self.store.iter_tree_contents(
1184
self.store[self.branch.head].tree):
1185
if not validate_path(entry.path):
1188
if S_ISGITLINK(entry.mode):
1189
pass # TODO(jelmer): record and return submodule paths
1191
# Let's at least try to use the working tree file:
1193
st = self._lstat(self.abspath(
1194
entry.path.decode('utf-8')))
1196
# But if it doesn't exist, we'll make something up.
1197
obj = self.store[entry.sha]
1198
st = os.stat_result((entry.mode, 0, 0, 0,
1200
obj.as_raw_string()), 0,
1202
(index, subpath) = self._lookup_index(entry.path)
1203
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1205
def _update_git_tree(self, old_revision, new_revision, change_reporter=None,
1207
basis_tree = self.revision_tree(old_revision)
1208
if new_revision != old_revision:
1209
with basis_tree.lock_read():
1210
new_basis_tree = self.branch.basis_tree()
1216
change_reporter=change_reporter,
1217
show_base=show_base)
1219
def pull(self, source, overwrite=False, stop_revision=None,
1220
change_reporter=None, possible_transports=None, local=False,
1222
with self.lock_write(), source.lock_read():
1223
old_revision = self.branch.last_revision()
1224
count = self.branch.pull(source, overwrite, stop_revision,
1225
possible_transports=possible_transports,
1227
self._update_git_tree(
1228
old_revision=old_revision,
1229
new_revision=self.branch.last_revision(),
1230
change_reporter=change_reporter,
1231
show_base=show_base)
1234
def add_reference(self, sub_tree):
1235
"""Add a TreeReference to the tree, pointing at sub_tree.
1237
:param sub_tree: subtree to add.
1239
with self.lock_tree_write():
1241
sub_tree_path = self.relpath(sub_tree.basedir)
1242
except errors.PathNotChild:
1243
raise BadReferenceTarget(
1244
self, sub_tree, 'Target not inside tree.')
1246
self._add([sub_tree_path], [None], ['tree-reference'])
1248
def _read_submodule_head(self, path):
1249
return read_submodule_head(self.abspath(path))
1251
def get_reference_revision(self, path):
1252
hexsha = self._read_submodule_head(path)
1254
return _mod_revision.NULL_REVISION
1255
return self.branch.lookup_foreign_revision_id(hexsha)
1257
def get_nested_tree(self, path):
1258
return workingtree.WorkingTree.open(self.abspath(path))
1260
def _directory_is_tree_reference(self, relpath):
1261
# as a special case, if a directory contains control files then
1262
# it's a tree reference, except that the root of the tree is not
1263
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1265
def extract(self, sub_path, format=None):
1266
"""Extract a subtree from this tree.
1268
A new branch will be created, relative to the path for this tree.
1271
segments = osutils.splitpath(path)
1272
transport = self.branch.controldir.root_transport
1273
for name in segments:
1274
transport = transport.clone(name)
1275
transport.ensure_base()
1278
with self.lock_tree_write():
1280
branch_transport = mkdirs(sub_path)
1282
format = self.controldir.cloning_metadir()
1283
branch_transport.ensure_base()
1284
branch_bzrdir = format.initialize_on_transport(branch_transport)
1286
repo = branch_bzrdir.find_repository()
1287
except errors.NoRepositoryPresent:
1288
repo = branch_bzrdir.create_repository()
1289
if not repo.supports_rich_root():
1290
raise errors.RootNotRich()
1291
new_branch = branch_bzrdir.create_branch()
1292
new_branch.pull(self.branch)
1293
for parent_id in self.get_parent_ids():
1294
new_branch.fetch(self.branch, parent_id)
1295
tree_transport = self.controldir.root_transport.clone(sub_path)
1296
if tree_transport.base != branch_transport.base:
1297
tree_bzrdir = format.initialize_on_transport(tree_transport)
1298
tree_bzrdir.set_branch_reference(new_branch)
1300
tree_bzrdir = branch_bzrdir
1301
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1302
wt.set_parent_ids(self.get_parent_ids())
1305
def _get_check_refs(self):
1306
"""Return the references needed to perform a check of this tree.
1308
The default implementation returns no refs, and is only suitable for
1309
trees that have no local caching and can commit on ghosts at any time.
1311
:seealso: breezy.check for details about check_refs.
1315
def copy_content_into(self, tree, revision_id=None):
1316
"""Copy the current content and user files of this tree into tree."""
1317
with self.lock_read():
1318
if revision_id is None:
1319
merge.transform_tree(tree, self)
1321
# TODO now merge from tree.last_revision to revision (to
1322
# preserve user local changes)
1324
other_tree = self.revision_tree(revision_id)
1325
except errors.NoSuchRevision:
1326
other_tree = self.branch.repository.revision_tree(
1329
merge.transform_tree(tree, other_tree)
1330
if revision_id == _mod_revision.NULL_REVISION:
1333
new_parents = [revision_id]
1334
tree.set_parent_ids(new_parents)
1337
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1339
_tree_class = GitWorkingTree
1341
supports_versioned_directories = False
1343
supports_setting_file_ids = False
1345
supports_store_uncommitted = False
1347
supports_leftmost_parent_id_as_ghost = False
1349
supports_righthand_parent_id_as_ghost = False
1351
requires_normalized_unicode_filenames = True
1353
supports_merge_modified = False
1355
ignore_filename = ".gitignore"
1358
def _matchingcontroldir(self):
1359
from .dir import LocalGitControlDirFormat
1360
return LocalGitControlDirFormat()
1362
def get_format_description(self):
1363
return "Git Working Tree"
1365
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1366
accelerator_tree=None, hardlink=False):
1367
"""See WorkingTreeFormat.initialize()."""
1368
if not isinstance(a_controldir, LocalGitDir):
1369
raise errors.IncompatibleFormat(self, a_controldir)
1370
branch = a_controldir.open_branch(nascent_ok=True)
1371
if revision_id is not None:
1372
branch.set_last_revision(revision_id)
1373
wt = GitWorkingTree(
1374
a_controldir, a_controldir.open_repository(), branch)
1375
for hook in MutableTree.hooks['post_build_tree']: