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 merge_modified(self):
215
def set_merge_modified(self, modified_hashes):
216
raise errors.UnsupportedOperation(self.set_merge_modified, self)
218
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
219
self.set_parent_ids([p for p, t in parents_list])
221
def _set_merges_from_parent_ids(self, rhs_parent_ids):
223
merges = [self.branch.lookup_bzr_revision_id(
224
revid)[0] for revid in rhs_parent_ids]
225
except errors.NoSuchRevision as e:
226
raise errors.GhostRevisionUnusableHere(e.revision)
228
self.control_transport.put_bytes(
229
'MERGE_HEAD', b'\n'.join(merges),
230
mode=self.controldir._get_file_mode())
233
self.control_transport.delete('MERGE_HEAD')
234
except errors.NoSuchFile:
237
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
238
"""Set the parent ids to revision_ids.
240
See also set_parent_trees. This api will try to retrieve the tree data
241
for each element of revision_ids from the trees repository. If you have
242
tree data already available, it is more efficient to use
243
set_parent_trees rather than set_parent_ids. set_parent_ids is however
244
an easier API to use.
246
:param revision_ids: The revision_ids to set as the parent ids of this
247
working tree. Any of these may be ghosts.
249
with self.lock_tree_write():
250
self._check_parents_for_ghosts(
251
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
252
for revision_id in revision_ids:
253
_mod_revision.check_not_reserved_id(revision_id)
255
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
257
if len(revision_ids) > 0:
258
self.set_last_revision(revision_ids[0])
260
self.set_last_revision(_mod_revision.NULL_REVISION)
262
self._set_merges_from_parent_ids(revision_ids[1:])
264
def get_parent_ids(self):
265
"""See Tree.get_parent_ids.
267
This implementation reads the pending merges list and last_revision
268
value and uses that to decide what the parents list should be.
270
last_rev = _mod_revision.ensure_null(self._last_revision())
271
if _mod_revision.NULL_REVISION == last_rev:
276
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
277
except errors.NoSuchFile:
280
for l in osutils.split_lines(merges_bytes):
281
revision_id = l.rstrip(b'\n')
283
self.branch.lookup_foreign_revision_id(revision_id))
286
def check_state(self):
287
"""Check that the working state is/isn't valid."""
290
def remove(self, files, verbose=False, to_file=None, keep_files=True,
292
"""Remove nominated files from the working tree metadata.
294
:param files: File paths relative to the basedir.
295
:param keep_files: If true, the files will also be kept.
296
:param force: Delete files and directories, even if they are changed
297
and even if the directories are not empty.
299
if not isinstance(files, list):
305
def backup(file_to_backup):
306
abs_path = self.abspath(file_to_backup)
307
backup_name = self.controldir._available_backup_name(
309
osutils.rename(abs_path, self.abspath(backup_name))
310
return "removed %s (but kept a copy: %s)" % (
311
file_to_backup, backup_name)
313
# Sort needed to first handle directory content before the directory
318
def recurse_directory_to_add_files(directory):
319
# Recurse directory and add all files
320
# so we can check if they have changed.
321
for parent_info, file_infos in self.walkdirs(directory):
322
for relpath, basename, kind, lstat, fileid, kind in file_infos:
323
# Is it versioned or ignored?
324
if self.is_versioned(relpath):
325
# Add nested content for deletion.
326
all_files.add(relpath)
328
# Files which are not versioned
329
# should be treated as unknown.
330
files_to_backup.append(relpath)
332
with self.lock_tree_write():
333
for filepath in files:
334
# Get file name into canonical form.
335
abspath = self.abspath(filepath)
336
filepath = self.relpath(abspath)
339
all_files.add(filepath)
340
recurse_directory_to_add_files(filepath)
342
files = list(all_files)
345
return # nothing to do
347
# Sort needed to first handle directory content before the
349
files.sort(reverse=True)
351
# Bail out if we are going to delete files we shouldn't
352
if not keep_files and not force:
353
for (file_id, path, content_change, versioned, parent_id, name,
354
kind, executable) in self.iter_changes(
355
self.basis_tree(), include_unchanged=True,
356
require_versioned=False, want_unversioned=True,
357
specific_files=files):
358
if versioned[0] is False:
359
# The record is unknown or newly added
360
files_to_backup.append(path[1])
361
files_to_backup.extend(
362
osutils.parent_directories(path[1]))
363
elif (content_change and (kind[1] is not None)
364
and osutils.is_inside_any(files, path[1])):
365
# Versioned and changed, but not deleted, and still
366
# in one of the dirs to be deleted.
367
files_to_backup.append(path[1])
368
files_to_backup.extend(
369
osutils.parent_directories(path[1]))
377
except errors.NoSuchFile:
380
abs_path = self.abspath(f)
382
# having removed it, it must be either ignored or unknown
383
if self.is_ignored(f):
387
kind_ch = osutils.kind_marker(kind)
388
to_file.write(new_status + ' ' + f + kind_ch + '\n')
390
message = "%s does not exist" % (f, )
393
if f in files_to_backup and not force:
396
if kind == 'directory':
397
osutils.rmtree(abs_path)
399
osutils.delete_any(abs_path)
400
message = "deleted %s" % (f,)
402
message = "removed %s" % (f,)
403
self._unversion_path(f)
405
# print only one message (if any) per file.
406
if message is not None:
408
self._versioned_dirs = None
410
def smart_add(self, file_list, recurse=True, action=None, save=True):
414
# expand any symlinks in the directory part, while leaving the
416
# only expanding if symlinks are supported avoids windows path bugs
417
if osutils.has_symlinks():
418
file_list = list(map(osutils.normalizepath, file_list))
420
conflicts_related = set()
421
for c in self.conflicts():
422
conflicts_related.update(c.associated_filenames())
428
def call_action(filepath, kind):
431
if action is not None:
432
parent_path = posixpath.dirname(filepath)
433
parent_id = self.path2id(parent_path)
434
parent_ie = self._get_dir_ie(parent_path, parent_id)
435
file_id = action(self, parent_ie, filepath, kind)
436
if file_id is not None:
437
raise workingtree.SettingFileIdUnsupported()
439
with self.lock_tree_write():
440
for filepath in osutils.canonical_relpaths(
441
self.basedir, file_list):
442
filepath, can_access = osutils.normalized_filename(filepath)
444
raise errors.InvalidNormalization(filepath)
446
abspath = self.abspath(filepath)
447
kind = osutils.file_kind(abspath)
448
if kind in ("file", "symlink"):
449
(index, subpath) = self._lookup_index(
450
filepath.encode('utf-8'))
454
call_action(filepath, kind)
456
self._index_add_entry(filepath, kind)
457
added.append(filepath)
458
elif kind == "directory":
459
(index, subpath) = self._lookup_index(
460
filepath.encode('utf-8'))
461
if subpath not in index:
462
call_action(filepath, kind)
464
user_dirs.append(filepath)
466
raise errors.BadFileKindError(filename=abspath, kind=kind)
467
for user_dir in user_dirs:
468
abs_user_dir = self.abspath(user_dir)
471
transport = _mod_transport.get_transport_from_path(
473
_mod_controldir.ControlDirFormat.find_format(transport)
475
except errors.NotBranchError:
477
except errors.UnsupportedFormatError:
482
trace.warning('skipping nested tree %r', abs_user_dir)
485
for name in os.listdir(abs_user_dir):
486
subp = os.path.join(user_dir, name)
487
if (self.is_control_filename(subp) or
488
self.mapping.is_special_file(subp)):
490
ignore_glob = self.is_ignored(subp)
491
if ignore_glob is not None:
492
ignored.setdefault(ignore_glob, []).append(subp)
494
abspath = self.abspath(subp)
495
kind = osutils.file_kind(abspath)
496
if kind == "directory":
497
user_dirs.append(subp)
499
(index, subpath) = self._lookup_index(
500
subp.encode('utf-8'))
504
if subp in conflicts_related:
506
call_action(subp, kind)
508
self._index_add_entry(subp, kind)
510
return added, ignored
512
def has_filename(self, filename):
513
return osutils.lexists(self.abspath(filename))
515
def _iter_files_recursive(self, from_dir=None, include_dirs=False):
518
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
519
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
520
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
521
if self.controldir.is_control_filename(
522
dir_relpath.decode(osutils._fs_enc)):
524
for name in list(dirnames):
525
if self.controldir.is_control_filename(
526
name.decode(osutils._fs_enc)):
527
dirnames.remove(name)
529
relpath = os.path.join(dir_relpath, name)
532
yield relpath.decode(osutils._fs_enc)
533
except UnicodeDecodeError:
534
raise errors.BadFilenameEncoding(
535
relpath, osutils._fs_enc)
536
if not self._has_dir(relpath):
537
dirnames.remove(name)
538
for name in filenames:
539
if not self.mapping.is_special_file(name):
540
yp = os.path.join(dir_relpath, name)
542
yield yp.decode(osutils._fs_enc)
543
except UnicodeDecodeError:
544
raise errors.BadFilenameEncoding(
548
"""Yield all unversioned files in this WorkingTree.
550
with self.lock_read():
552
[p.decode('utf-8') for p, i in self._recurse_index_entries()])
553
all_paths = set(self._iter_files_recursive(include_dirs=False))
554
return iter(all_paths - index_paths)
556
def _gather_kinds(self, files, kinds):
557
"""See MutableTree._gather_kinds."""
558
with self.lock_tree_write():
559
for pos, f in enumerate(files):
560
if kinds[pos] is None:
561
fullpath = osutils.normpath(self.abspath(f))
563
kind = osutils.file_kind(fullpath)
565
if e.errno == errno.ENOENT:
566
raise errors.NoSuchFile(fullpath)
567
if (kind == 'directory' and f != '' and
568
os.path.exists(os.path.join(fullpath, '.git'))):
569
kind = 'tree-reference'
573
if self._lock_mode != 'w':
574
raise errors.NotWriteLocked(self)
575
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
576
# already in use and GitFile doesn't allow overriding the lock file
578
f = open(self.control_transport.local_abspath('index'), 'wb')
579
# Note that _flush will close the file
585
write_index_dict(shaf, self.index)
587
except BaseException:
590
self._index_dirty = False
592
def has_or_had_id(self, file_id):
593
if self.has_id(file_id):
595
if self.had_id(file_id):
599
def had_id(self, file_id):
600
path = self._basis_fileid_map.lookup_path(file_id)
602
head = self.repository._git.head()
604
# Assume no if basis is not accessible
607
root_tree = self.store[head].tree
611
tree_lookup_path(self.store.__getitem__,
612
root_tree, path.encode('utf-8'))
618
def get_file_mtime(self, path):
619
"""See Tree.get_file_mtime."""
621
return self._lstat(path).st_mtime
623
if e.errno == errno.ENOENT:
624
raise errors.NoSuchFile(path)
627
def is_ignored(self, filename):
628
r"""Check whether the filename matches an ignore pattern.
630
If the file is ignored, returns the pattern which caused it to
631
be ignored, otherwise None. So this can simply be used as a
632
boolean if desired."""
633
if getattr(self, '_global_ignoreglobster', None) is None:
635
ignore_globs.update(ignores.get_runtime_ignores())
636
ignore_globs.update(ignores.get_user_ignores())
637
self._global_ignoreglobster = globbing.ExceptionGlobster(
639
match = self._global_ignoreglobster.match(filename)
640
if match is not None:
643
if self.kind(filename) == 'directory':
645
except errors.NoSuchFile:
647
filename = filename.lstrip('/')
648
ignore_manager = self._get_ignore_manager()
649
ps = list(ignore_manager.find_matching(filename))
652
if not ps[-1].is_exclude:
656
def _get_ignore_manager(self):
657
ignoremanager = getattr(self, '_ignoremanager', None)
658
if ignoremanager is not None:
661
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
662
self._ignoremanager = ignore_manager
663
return ignore_manager
665
def _flush_ignore_list_cache(self):
666
self._ignoremanager = None
668
def set_last_revision(self, revid):
669
if _mod_revision.is_null(revid):
670
self.branch.set_last_revision_info(0, revid)
672
_mod_revision.check_not_reserved_id(revid)
674
self.branch.generate_revision_history(revid)
675
except errors.NoSuchRevision:
676
raise errors.GhostRevisionUnusableHere(revid)
678
def _reset_data(self):
680
head = self.repository._git.head()
682
self._basis_fileid_map = GitFileIdMap({}, self.mapping)
684
self._basis_fileid_map = self.mapping.get_fileid_map(
685
self.store.__getitem__, self.store[head].tree)
686
self._fileid_map = self._basis_fileid_map.copy()
688
def get_file_verifier(self, path, stat_value=None):
689
with self.lock_read():
690
(index, subpath) = self._lookup_index(path.encode('utf-8'))
692
return ("GIT", index[subpath].sha)
694
if self._has_dir(path):
696
raise errors.NoSuchFile(path)
698
def get_file_sha1(self, path, stat_value=None):
699
with self.lock_read():
700
if not self.is_versioned(path):
701
raise errors.NoSuchFile(path)
702
abspath = self.abspath(path)
704
return osutils.sha_file_by_name(abspath)
706
if e.errno in (errno.EISDIR, errno.ENOENT):
710
def revision_tree(self, revid):
711
return self.repository.revision_tree(revid)
713
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
714
mode = stat_result.st_mode
715
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
717
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
718
return self.basis_tree().is_executable(path)
720
def stored_kind(self, path):
721
with self.lock_read():
722
encoded_path = path.encode('utf-8')
723
(index, subpath) = self._lookup_index(encoded_path)
725
return mode_kind(index[subpath].mode)
727
# Maybe it's a directory?
728
if self._has_dir(encoded_path):
730
raise errors.NoSuchFile(path)
732
def _lstat(self, path):
733
return os.lstat(self.abspath(path))
735
def _live_entry(self, path):
736
encoded_path = self.abspath(path.decode('utf-8')).encode(
738
return index_entry_from_path(encoded_path)
740
def is_executable(self, path):
741
with self.lock_read():
742
if getattr(self, "_supports_executable",
743
osutils.supports_executable)():
744
mode = self._lstat(path).st_mode
746
(index, subpath) = self._lookup_index(path.encode('utf-8'))
748
mode = index[subpath].mode
751
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
753
def _is_executable_from_path_and_stat(self, path, stat_result):
754
if getattr(self, "_supports_executable",
755
osutils.supports_executable)():
756
return self._is_executable_from_path_and_stat_from_stat(
759
return self._is_executable_from_path_and_stat_from_basis(
762
def list_files(self, include_root=False, from_dir=None, recursive=True):
763
if from_dir is None or from_dir == '.':
766
fk_entries = {'directory': tree.TreeDirectory,
767
'file': tree.TreeFile,
768
'symlink': tree.TreeLink,
769
'tree-reference': tree.TreeReference}
770
with self.lock_read():
771
root_ie = self._get_dir_ie(u"", None)
772
if include_root and not from_dir:
773
yield "", "V", root_ie.kind, root_ie.file_id, root_ie
774
dir_ids[u""] = root_ie.file_id
776
path_iterator = sorted(
777
self._iter_files_recursive(from_dir, include_dirs=True))
779
encoded_from_dir = self.abspath(from_dir).encode(
781
path_iterator = sorted(
782
[os.path.join(from_dir, name.decode(osutils._fs_enc))
783
for name in os.listdir(encoded_from_dir)
784
if not self.controldir.is_control_filename(
785
name.decode(osutils._fs_enc)) and
786
not self.mapping.is_special_file(
787
name.decode(osutils._fs_enc))])
788
for path in path_iterator:
790
encoded_path = path.encode("utf-8")
791
except UnicodeEncodeError:
792
raise errors.BadFilenameEncoding(
793
path, osutils._fs_enc)
794
(index, index_path) = self._lookup_index(encoded_path)
796
value = index[index_path]
799
kind = self.kind(path)
800
parent, name = posixpath.split(path)
801
for dir_path, dir_ie in self._add_missing_parent_ids(
804
if kind in ('directory', 'tree-reference'):
806
if self._has_dir(encoded_path):
807
ie = self._get_dir_ie(path, self.path2id(path))
810
elif self.is_ignored(path):
812
ie = fk_entries[kind]()
816
ie = fk_entries[kind]()
819
posixpath.relpath(path, from_dir), status, kind,
822
if value is not None:
823
ie = self._get_file_ie(name, path, value, dir_ids[parent])
824
yield (posixpath.relpath(path, from_dir), "V", ie.kind,
827
ie = fk_entries[kind]()
828
yield (posixpath.relpath(path, from_dir), ("I" if
829
self.is_ignored(path) else "?"), kind, None, ie)
831
def all_file_ids(self):
832
raise errors.UnsupportedOperation(self.all_file_ids, self)
834
def all_versioned_paths(self):
835
with self.lock_read():
837
for path in self.index:
838
if self.mapping.is_special_file(path):
840
path = path.decode("utf-8")
843
path = posixpath.dirname(path).strip("/")
849
def iter_child_entries(self, path):
850
encoded_path = path.encode('utf-8')
851
with self.lock_read():
852
parent_id = self.path2id(path)
854
for item_path, value in self.index.iteritems():
855
decoded_item_path = item_path.decode('utf-8')
856
if self.mapping.is_special_file(item_path):
858
if not osutils.is_inside(path, decoded_item_path):
861
subpath = posixpath.relpath(decoded_item_path, path)
863
dirname = subpath.split('/', 1)[0]
864
file_ie = self._get_dir_ie(
865
posixpath.join(path, dirname), parent_id)
867
(unused_parent, name) = posixpath.split(decoded_item_path)
868
file_ie = self._get_file_ie(
869
name, decoded_item_path, value, parent_id)
871
if not found_any and path != u'':
872
raise errors.NoSuchFile(path)
875
with self.lock_read():
876
conflicts = _mod_conflicts.ConflictList()
877
for item_path, value in self.index.iteritems():
878
if value.flags & FLAG_STAGEMASK:
879
conflicts.append(_mod_conflicts.TextConflict(
880
item_path.decode('utf-8')))
883
def set_conflicts(self, conflicts):
885
for conflict in conflicts:
886
if conflict.typestring in ('text conflict', 'contents conflict'):
887
by_path.add(conflict.path.encode('utf-8'))
889
raise errors.UnsupportedOperation(self.set_conflicts, self)
890
with self.lock_tree_write():
891
for path in self.index:
892
self._set_conflicted(path, path in by_path)
894
def _set_conflicted(self, path, conflicted):
895
trace.mutter('change conflict: %r -> %r', path, conflicted)
896
value = self.index[path]
897
self._index_dirty = True
899
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
901
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
903
def add_conflicts(self, new_conflicts):
904
with self.lock_tree_write():
905
for conflict in new_conflicts:
906
if conflict.typestring in ('text conflict',
907
'contents conflict'):
909
self._set_conflicted(
910
conflict.path.encode('utf-8'), True)
912
raise errors.UnsupportedOperation(
913
self.add_conflicts, self)
915
raise errors.UnsupportedOperation(self.add_conflicts, self)
917
def walkdirs(self, prefix=""):
918
"""Walk the directories of this tree.
920
returns a generator which yields items in the form:
921
((curren_directory_path, fileid),
922
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
925
This API returns a generator, which is only valid during the current
926
tree transaction - within a single lock_read or lock_write duration.
928
If the tree is not locked, it may cause an error to be raised,
929
depending on the tree implementation.
931
from bisect import bisect_left
933
disk_top = self.abspath(prefix)
934
if disk_top.endswith('/'):
935
disk_top = disk_top[:-1]
936
top_strip_len = len(disk_top) + 1
937
inventory_iterator = self._walkdirs(prefix)
938
disk_iterator = osutils.walkdirs(disk_top, prefix)
940
current_disk = next(disk_iterator)
941
disk_finished = False
943
if not (e.errno == errno.ENOENT
944
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
949
current_inv = next(inventory_iterator)
951
except StopIteration:
954
while not inv_finished or not disk_finished:
956
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
957
cur_disk_dir_content) = current_disk
959
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
960
cur_disk_dir_content) = ((None, None), None)
961
if not disk_finished:
962
# strip out .bzr dirs
963
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
964
and len(cur_disk_dir_content) > 0):
965
# osutils.walkdirs can be made nicer -
966
# yield the path-from-prefix rather than the pathjoined
968
bzrdir_loc = bisect_left(cur_disk_dir_content,
970
if (bzrdir_loc < len(cur_disk_dir_content) and
971
self.controldir.is_control_filename(
972
cur_disk_dir_content[bzrdir_loc][0])):
973
# we dont yield the contents of, or, .bzr itself.
974
del cur_disk_dir_content[bzrdir_loc]
976
# everything is unknown
979
# everything is missing
982
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
983
- (current_inv[0][0] < cur_disk_dir_relpath))
985
# disk is before inventory - unknown
986
dirblock = [(relpath, basename, kind, stat, None, None) for
987
relpath, basename, kind, stat, top_path in
988
cur_disk_dir_content]
989
yield (cur_disk_dir_relpath, None), dirblock
991
current_disk = next(disk_iterator)
992
except StopIteration:
995
# inventory is before disk - missing.
996
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
997
for relpath, basename, dkind, stat, fileid, kind in
999
yield (current_inv[0][0], current_inv[0][1]), dirblock
1001
current_inv = next(inventory_iterator)
1002
except StopIteration:
1005
# versioned present directory
1006
# merge the inventory and disk data together
1008
for relpath, subiterator in itertools.groupby(sorted(
1009
current_inv[1] + cur_disk_dir_content,
1010
key=operator.itemgetter(0)), operator.itemgetter(1)):
1011
path_elements = list(subiterator)
1012
if len(path_elements) == 2:
1013
inv_row, disk_row = path_elements
1014
# versioned, present file
1015
dirblock.append((inv_row[0],
1016
inv_row[1], disk_row[2],
1017
disk_row[3], inv_row[4],
1019
elif len(path_elements[0]) == 5:
1022
(path_elements[0][0], path_elements[0][1],
1023
path_elements[0][2], path_elements[0][3],
1025
elif len(path_elements[0]) == 6:
1026
# versioned, absent file.
1028
(path_elements[0][0], path_elements[0][1],
1029
'unknown', None, path_elements[0][4],
1030
path_elements[0][5]))
1032
raise NotImplementedError('unreachable code')
1033
yield current_inv[0], dirblock
1035
current_inv = next(inventory_iterator)
1036
except StopIteration:
1039
current_disk = next(disk_iterator)
1040
except StopIteration:
1041
disk_finished = True
1043
def _walkdirs(self, prefix=u""):
1046
prefix = prefix.encode('utf-8')
1047
per_dir = defaultdict(set)
1049
per_dir[(u'', self.get_root_id())] = set()
1051
def add_entry(path, kind):
1052
if path == b'' or not path.startswith(prefix):
1054
(dirname, child_name) = posixpath.split(path)
1055
add_entry(dirname, 'directory')
1056
dirname = dirname.decode("utf-8")
1057
dir_file_id = self.path2id(dirname)
1058
if not isinstance(value, tuple) or len(value) != 10:
1059
raise ValueError(value)
1060
per_dir[(dirname, dir_file_id)].add(
1061
(path.decode("utf-8"), child_name.decode("utf-8"),
1063
self.path2id(path.decode("utf-8")),
1065
with self.lock_read():
1066
for path, value in self.index.iteritems():
1067
if self.mapping.is_special_file(path):
1069
if not path.startswith(prefix):
1071
add_entry(path, mode_kind(value.mode))
1072
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1074
def get_shelf_manager(self):
1075
raise workingtree.ShelvingUnsupported()
1077
def store_uncommitted(self):
1078
raise errors.StoringUncommittedNotSupported(self)
1080
def apply_inventory_delta(self, changes):
1081
for (old_path, new_path, file_id, ie) in changes:
1082
if old_path is not None:
1083
(index, old_subpath) = self._lookup_index(
1084
old_path.encode('utf-8'))
1086
self._index_del_entry(index, old_subpath)
1090
self._versioned_dirs = None
1091
if new_path is not None and ie.kind != 'directory':
1092
if ie.kind == 'tree-reference':
1093
self._index_add_entry(
1095
reference_revision=ie.reference_revision)
1097
self._index_add_entry(new_path, ie.kind)
1100
def annotate_iter(self, path,
1101
default_revision=_mod_revision.CURRENT_REVISION):
1102
"""See Tree.annotate_iter
1104
This implementation will use the basis tree implementation if possible.
1105
Lines not in the basis are attributed to CURRENT_REVISION
1107
If there are pending merges, lines added by those merges will be
1108
incorrectly attributed to CURRENT_REVISION (but after committing, the
1109
attribution will be correct).
1111
with self.lock_read():
1112
maybe_file_parent_keys = []
1113
for parent_id in self.get_parent_ids():
1115
parent_tree = self.revision_tree(parent_id)
1116
except errors.NoSuchRevisionInTree:
1117
parent_tree = self.branch.repository.revision_tree(
1119
with parent_tree.lock_read():
1120
# TODO(jelmer): Use rename/copy tracker to find path name
1124
kind = parent_tree.kind(parent_path)
1125
except errors.NoSuchFile:
1128
# Note: this is slightly unnecessary, because symlinks
1129
# and directories have a "text" which is the empty
1130
# text, and we know that won't mess up annotations. But
1135
parent_tree.get_file_revision(parent_path))
1136
if parent_text_key not in maybe_file_parent_keys:
1137
maybe_file_parent_keys.append(parent_text_key)
1138
# Now we have the parents of this content
1139
from breezy.annotate import Annotator
1140
from .annotate import AnnotateProvider
1141
annotate_provider = AnnotateProvider(
1142
self.branch.repository._file_change_scanner)
1143
annotator = Annotator(annotate_provider)
1145
from breezy.graph import Graph
1146
graph = Graph(annotate_provider)
1147
heads = graph.heads(maybe_file_parent_keys)
1148
file_parent_keys = []
1149
for key in maybe_file_parent_keys:
1151
file_parent_keys.append(key)
1153
text = self.get_file_text(path)
1154
this_key = (path, default_revision)
1155
annotator.add_special_text(this_key, file_parent_keys, text)
1156
annotations = [(key[-1], line)
1157
for key, line in annotator.annotate_flat(this_key)]
1160
def _rename_one(self, from_rel, to_rel):
1161
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1163
def _build_checkout_with_index(self):
1164
build_index_from_tree(
1165
self.user_transport.local_abspath('.'),
1166
self.control_transport.local_abspath("index"),
1169
if self.branch.head is None
1170
else self.store[self.branch.head].tree)
1172
def reset_state(self, revision_ids=None):
1173
"""Reset the state of the working tree.
1175
This does a hard-reset to a last-known-good state. This is a way to
1176
fix if something got corrupted (like the .git/index file)
1178
with self.lock_tree_write():
1179
if revision_ids is not None:
1180
self.set_parent_ids(revision_ids)
1182
self._index_dirty = True
1183
if self.branch.head is not None:
1184
for entry in self.store.iter_tree_contents(
1185
self.store[self.branch.head].tree):
1186
if not validate_path(entry.path):
1189
if S_ISGITLINK(entry.mode):
1190
pass # TODO(jelmer): record and return submodule paths
1192
# Let's at least try to use the working tree file:
1194
st = self._lstat(self.abspath(
1195
entry.path.decode('utf-8')))
1197
# But if it doesn't exist, we'll make something up.
1198
obj = self.store[entry.sha]
1199
st = os.stat_result((entry.mode, 0, 0, 0,
1201
obj.as_raw_string()), 0,
1203
(index, subpath) = self._lookup_index(entry.path)
1204
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1206
def pull(self, source, overwrite=False, stop_revision=None,
1207
change_reporter=None, possible_transports=None, local=False,
1209
with self.lock_write(), source.lock_read():
1210
old_revision = self.branch.last_revision()
1211
basis_tree = self.basis_tree()
1212
count = self.branch.pull(source, overwrite, stop_revision,
1213
possible_transports=possible_transports,
1215
new_revision = self.branch.last_revision()
1216
if new_revision != old_revision:
1217
with basis_tree.lock_read():
1218
new_basis_tree = self.branch.basis_tree()
1224
change_reporter=change_reporter,
1225
show_base=show_base)
1228
def add_reference(self, sub_tree):
1229
"""Add a TreeReference to the tree, pointing at sub_tree.
1231
:param sub_tree: subtree to add.
1233
with self.lock_tree_write():
1235
sub_tree_path = self.relpath(sub_tree.basedir)
1236
except errors.PathNotChild:
1237
raise BadReferenceTarget(
1238
self, sub_tree, 'Target not inside tree.')
1240
self._add([sub_tree_path], [None], ['tree-reference'])
1242
def _read_submodule_head(self, path):
1243
return read_submodule_head(self.abspath(path))
1245
def get_reference_revision(self, path):
1246
hexsha = self._read_submodule_head(path)
1248
return _mod_revision.NULL_REVISION
1249
return self.branch.lookup_foreign_revision_id(hexsha)
1251
def get_nested_tree(self, path):
1252
return workingtree.WorkingTree.open(self.abspath(path))
1254
def _directory_is_tree_reference(self, relpath):
1255
# as a special case, if a directory contains control files then
1256
# it's a tree reference, except that the root of the tree is not
1257
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1259
def extract(self, sub_path, format=None):
1260
"""Extract a subtree from this tree.
1262
A new branch will be created, relative to the path for this tree.
1265
segments = osutils.splitpath(path)
1266
transport = self.branch.controldir.root_transport
1267
for name in segments:
1268
transport = transport.clone(name)
1269
transport.ensure_base()
1272
with self.lock_tree_write():
1274
branch_transport = mkdirs(sub_path)
1276
format = self.controldir.cloning_metadir()
1277
branch_transport.ensure_base()
1278
branch_bzrdir = format.initialize_on_transport(branch_transport)
1280
repo = branch_bzrdir.find_repository()
1281
except errors.NoRepositoryPresent:
1282
repo = branch_bzrdir.create_repository()
1283
if not repo.supports_rich_root():
1284
raise errors.RootNotRich()
1285
new_branch = branch_bzrdir.create_branch()
1286
new_branch.pull(self.branch)
1287
for parent_id in self.get_parent_ids():
1288
new_branch.fetch(self.branch, parent_id)
1289
tree_transport = self.controldir.root_transport.clone(sub_path)
1290
if tree_transport.base != branch_transport.base:
1291
tree_bzrdir = format.initialize_on_transport(tree_transport)
1292
tree_bzrdir.set_branch_reference(new_branch)
1294
tree_bzrdir = branch_bzrdir
1295
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1296
wt.set_parent_ids(self.get_parent_ids())
1299
def _get_check_refs(self):
1300
"""Return the references needed to perform a check of this tree.
1302
The default implementation returns no refs, and is only suitable for
1303
trees that have no local caching and can commit on ghosts at any time.
1305
:seealso: breezy.check for details about check_refs.
1309
def copy_content_into(self, tree, revision_id=None):
1310
"""Copy the current content and user files of this tree into tree."""
1311
with self.lock_read():
1312
if revision_id is None:
1313
merge.transform_tree(tree, self)
1315
# TODO now merge from tree.last_revision to revision (to
1316
# preserve user local changes)
1318
other_tree = self.revision_tree(revision_id)
1319
except errors.NoSuchRevision:
1320
other_tree = self.branch.repository.revision_tree(
1323
merge.transform_tree(tree, other_tree)
1324
if revision_id == _mod_revision.NULL_REVISION:
1327
new_parents = [revision_id]
1328
tree.set_parent_ids(new_parents)
1331
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1333
_tree_class = GitWorkingTree
1335
supports_versioned_directories = False
1337
supports_setting_file_ids = False
1339
supports_store_uncommitted = False
1341
supports_leftmost_parent_id_as_ghost = False
1343
supports_righthand_parent_id_as_ghost = False
1345
requires_normalized_unicode_filenames = True
1347
supports_merge_modified = False
1349
ignore_filename = ".gitignore"
1352
def _matchingcontroldir(self):
1353
from .dir import LocalGitControlDirFormat
1354
return LocalGitControlDirFormat()
1356
def get_format_description(self):
1357
return "Git Working Tree"
1359
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1360
accelerator_tree=None, hardlink=False):
1361
"""See WorkingTreeFormat.initialize()."""
1362
if not isinstance(a_controldir, LocalGitDir):
1363
raise errors.IncompatibleFormat(self, a_controldir)
1364
branch = a_controldir.open_branch(nascent_ok=True)
1365
if revision_id is not None:
1366
branch.set_last_revision(revision_id)
1367
wt = GitWorkingTree(
1368
a_controldir, a_controldir.open_repository(), branch)
1369
for hook in MutableTree.hooks['post_build_tree']: