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 (
86
class GitWorkingTree(MutableGitIndexTree, workingtree.WorkingTree):
87
"""A Git working tree."""
89
def __init__(self, controldir, repo, branch):
90
MutableGitIndexTree.__init__(self)
91
basedir = controldir.root_transport.local_abspath('.')
92
self.basedir = osutils.realpath(basedir)
93
self.controldir = controldir
94
self.repository = repo
95
self.store = self.repository._git.object_store
96
self.mapping = self.repository.get_mapping()
98
self._transport = self.repository._git._controltransport
99
self._format = GitWorkingTreeFormat()
101
self._index_file = None
102
self.views = self._make_views()
103
self._rules_searcher = None
104
self._detect_case_handling()
107
def supports_tree_reference(self):
110
def supports_rename_tracking(self):
113
def _read_index(self):
114
self.index = Index(self.control_transport.local_abspath('index'))
115
self._index_dirty = False
118
"""Lock the repository for read operations.
120
:return: A breezy.lock.LogicalLockResult.
122
if not self._lock_mode:
123
self._lock_mode = 'r'
127
self._lock_count += 1
128
self.branch.lock_read()
129
return lock.LogicalLockResult(self.unlock)
131
def _lock_write_tree(self):
132
if not self._lock_mode:
133
self._lock_mode = 'w'
136
self._index_file = GitFile(
137
self.control_transport.local_abspath('index'), 'wb')
139
raise errors.LockContention('index')
141
elif self._lock_mode == 'r':
142
raise errors.ReadOnlyError(self)
144
self._lock_count += 1
146
def lock_tree_write(self):
147
self.branch.lock_read()
149
self._lock_write_tree()
150
return lock.LogicalLockResult(self.unlock)
151
except BaseException:
155
def lock_write(self, token=None):
156
self.branch.lock_write()
158
self._lock_write_tree()
159
return lock.LogicalLockResult(self.unlock)
160
except BaseException:
165
return self._lock_count >= 1
167
def get_physical_lock_status(self):
170
def break_lock(self):
172
self.control_transport.delete('index.lock')
173
except errors.NoSuchFile:
175
self.branch.break_lock()
177
@only_raises(errors.LockNotHeld, errors.LockBroken)
179
if not self._lock_count:
180
return lock.cant_unlock_not_held(self)
183
self._lock_count -= 1
184
if self._lock_count > 0:
186
if self._index_file is not None:
187
if self._index_dirty:
188
self._flush(self._index_file)
189
self._index_file.close()
191
# Something else already triggered a write of the index
192
# file by calling .flush()
193
self._index_file.abort()
194
self._index_file = None
195
self._lock_mode = None
203
def _detect_case_handling(self):
205
self._transport.stat(".git/cOnFiG")
206
except errors.NoSuchFile:
207
self.case_sensitive = True
209
self.case_sensitive = False
211
def get_transform(self, pb=None):
212
from ..transform import TreeTransform
213
return TreeTransform(self, pb=pb)
215
def merge_modified(self):
218
def set_merge_modified(self, modified_hashes):
219
raise errors.UnsupportedOperation(self.set_merge_modified, self)
221
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
222
self.set_parent_ids([p for p, t in parents_list])
224
def _set_merges_from_parent_ids(self, rhs_parent_ids):
226
merges = [self.branch.lookup_bzr_revision_id(
227
revid)[0] for revid in rhs_parent_ids]
228
except errors.NoSuchRevision as e:
229
raise errors.GhostRevisionUnusableHere(e.revision)
231
self.control_transport.put_bytes(
232
'MERGE_HEAD', b'\n'.join(merges),
233
mode=self.controldir._get_file_mode())
236
self.control_transport.delete('MERGE_HEAD')
237
except errors.NoSuchFile:
240
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
241
"""Set the parent ids to revision_ids.
243
See also set_parent_trees. This api will try to retrieve the tree data
244
for each element of revision_ids from the trees repository. If you have
245
tree data already available, it is more efficient to use
246
set_parent_trees rather than set_parent_ids. set_parent_ids is however
247
an easier API to use.
249
:param revision_ids: The revision_ids to set as the parent ids of this
250
working tree. Any of these may be ghosts.
252
with self.lock_tree_write():
253
self._check_parents_for_ghosts(
254
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
255
for revision_id in revision_ids:
256
_mod_revision.check_not_reserved_id(revision_id)
258
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
260
if len(revision_ids) > 0:
261
self.set_last_revision(revision_ids[0])
263
self.set_last_revision(_mod_revision.NULL_REVISION)
265
self._set_merges_from_parent_ids(revision_ids[1:])
267
def get_parent_ids(self):
268
"""See Tree.get_parent_ids.
270
This implementation reads the pending merges list and last_revision
271
value and uses that to decide what the parents list should be.
273
last_rev = _mod_revision.ensure_null(self._last_revision())
274
if _mod_revision.NULL_REVISION == last_rev:
279
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
280
except errors.NoSuchFile:
283
for l in osutils.split_lines(merges_bytes):
284
revision_id = l.rstrip(b'\n')
286
self.branch.lookup_foreign_revision_id(revision_id))
289
def check_state(self):
290
"""Check that the working state is/isn't valid."""
293
def remove(self, files, verbose=False, to_file=None, keep_files=True,
295
"""Remove nominated files from the working tree metadata.
297
:param files: File paths relative to the basedir.
298
:param keep_files: If true, the files will also be kept.
299
:param force: Delete files and directories, even if they are changed
300
and even if the directories are not empty.
302
if not isinstance(files, list):
308
def backup(file_to_backup):
309
abs_path = self.abspath(file_to_backup)
310
backup_name = self.controldir._available_backup_name(
312
osutils.rename(abs_path, self.abspath(backup_name))
313
return "removed %s (but kept a copy: %s)" % (
314
file_to_backup, backup_name)
316
# Sort needed to first handle directory content before the directory
321
def recurse_directory_to_add_files(directory):
322
# Recurse directory and add all files
323
# so we can check if they have changed.
324
for parent_info, file_infos in self.walkdirs(directory):
325
for relpath, basename, kind, lstat, fileid, kind in file_infos:
326
# Is it versioned or ignored?
327
if self.is_versioned(relpath):
328
# Add nested content for deletion.
329
all_files.add(relpath)
331
# Files which are not versioned
332
# should be treated as unknown.
333
files_to_backup.append(relpath)
335
with self.lock_tree_write():
336
for filepath in files:
337
# Get file name into canonical form.
338
abspath = self.abspath(filepath)
339
filepath = self.relpath(abspath)
342
all_files.add(filepath)
343
recurse_directory_to_add_files(filepath)
345
files = list(all_files)
348
return # nothing to do
350
# Sort needed to first handle directory content before the
352
files.sort(reverse=True)
354
# Bail out if we are going to delete files we shouldn't
355
if not keep_files and not force:
356
for change in self.iter_changes(
357
self.basis_tree(), include_unchanged=True,
358
require_versioned=False, want_unversioned=True,
359
specific_files=files):
360
if change.versioned[0] is False:
361
# The record is unknown or newly added
362
files_to_backup.append(change.path[1])
363
files_to_backup.extend(
364
osutils.parent_directories(change.path[1]))
365
elif (change.changed_content and (change.kind[1] is not None)
366
and osutils.is_inside_any(files, change.path[1])):
367
# Versioned and changed, but not deleted, and still
368
# in one of the dirs to be deleted.
369
files_to_backup.append(change.path[1])
370
files_to_backup.extend(
371
osutils.parent_directories(change.path[1]))
379
except errors.NoSuchFile:
382
abs_path = self.abspath(f)
384
# having removed it, it must be either ignored or unknown
385
if self.is_ignored(f):
389
kind_ch = osutils.kind_marker(kind)
390
to_file.write(new_status + ' ' + f + kind_ch + '\n')
392
message = "%s does not exist" % (f, )
395
if f in files_to_backup and not force:
398
if kind == 'directory':
399
osutils.rmtree(abs_path)
401
osutils.delete_any(abs_path)
402
message = "deleted %s" % (f,)
404
message = "removed %s" % (f,)
405
self._unversion_path(f)
407
# print only one message (if any) per file.
408
if message is not None:
410
self._versioned_dirs = None
412
def smart_add(self, file_list, recurse=True, action=None, save=True):
416
# expand any symlinks in the directory part, while leaving the
418
# only expanding if symlinks are supported avoids windows path bugs
419
if self.supports_symlinks():
420
file_list = list(map(osutils.normalizepath, file_list))
422
conflicts_related = set()
423
for c in self.conflicts():
424
conflicts_related.update(c.associated_filenames())
430
def call_action(filepath, kind):
433
if action is not None:
434
parent_path = posixpath.dirname(filepath)
435
parent_id = self.path2id(parent_path)
436
parent_ie = self._get_dir_ie(parent_path, parent_id)
437
file_id = action(self, parent_ie, filepath, kind)
438
if file_id is not None:
439
raise workingtree.SettingFileIdUnsupported()
441
with self.lock_tree_write():
442
for filepath in osutils.canonical_relpaths(
443
self.basedir, file_list):
444
filepath, can_access = osutils.normalized_filename(filepath)
446
raise errors.InvalidNormalization(filepath)
448
abspath = self.abspath(filepath)
449
kind = osutils.file_kind(abspath)
450
if kind in ("file", "symlink"):
451
(index, subpath) = self._lookup_index(
452
filepath.encode('utf-8'))
456
call_action(filepath, kind)
458
self._index_add_entry(filepath, kind)
459
added.append(filepath)
460
elif kind == "directory":
461
(index, subpath) = self._lookup_index(
462
filepath.encode('utf-8'))
463
if subpath not in index:
464
call_action(filepath, kind)
466
user_dirs.append(filepath)
468
raise errors.BadFileKindError(filename=abspath, kind=kind)
469
for user_dir in user_dirs:
470
abs_user_dir = self.abspath(user_dir)
473
transport = _mod_transport.get_transport_from_path(
475
_mod_controldir.ControlDirFormat.find_format(transport)
477
except errors.NotBranchError:
479
except errors.UnsupportedFormatError:
484
trace.warning('skipping nested tree %r', abs_user_dir)
487
for name in os.listdir(abs_user_dir):
488
subp = os.path.join(user_dir, name)
489
if (self.is_control_filename(subp) or
490
self.mapping.is_special_file(subp)):
492
ignore_glob = self.is_ignored(subp)
493
if ignore_glob is not None:
494
ignored.setdefault(ignore_glob, []).append(subp)
496
abspath = self.abspath(subp)
497
kind = osutils.file_kind(abspath)
498
if kind == "directory":
499
user_dirs.append(subp)
501
(index, subpath) = self._lookup_index(
502
subp.encode('utf-8'))
506
if subp in conflicts_related:
508
call_action(subp, kind)
510
self._index_add_entry(subp, kind)
512
return added, ignored
514
def has_filename(self, filename):
515
return osutils.lexists(self.abspath(filename))
517
def _iter_files_recursive(self, from_dir=None, include_dirs=False):
520
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
521
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
522
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
523
if self.controldir.is_control_filename(
524
dir_relpath.decode(osutils._fs_enc)):
526
for name in list(dirnames):
527
if self.controldir.is_control_filename(
528
name.decode(osutils._fs_enc)):
529
dirnames.remove(name)
531
relpath = os.path.join(dir_relpath, name)
534
yield relpath.decode(osutils._fs_enc)
535
except UnicodeDecodeError:
536
raise errors.BadFilenameEncoding(
537
relpath, osutils._fs_enc)
538
if not self._has_dir(relpath):
539
dirnames.remove(name)
540
for name in filenames:
541
if self.mapping.is_special_file(name):
543
if self.controldir.is_control_filename(
544
name.decode(osutils._fs_enc, 'replace')):
546
yp = os.path.join(dir_relpath, name)
548
yield yp.decode(osutils._fs_enc)
549
except UnicodeDecodeError:
550
raise errors.BadFilenameEncoding(
554
"""Yield all unversioned files in this WorkingTree.
556
with self.lock_read():
558
[p.decode('utf-8') for p, i in self._recurse_index_entries()])
559
all_paths = set(self._iter_files_recursive(include_dirs=False))
560
return iter(all_paths - index_paths)
562
def _gather_kinds(self, files, kinds):
563
"""See MutableTree._gather_kinds."""
564
with self.lock_tree_write():
565
for pos, f in enumerate(files):
566
if kinds[pos] is None:
567
fullpath = osutils.normpath(self.abspath(f))
569
kind = osutils.file_kind(fullpath)
571
if e.errno == errno.ENOENT:
572
raise errors.NoSuchFile(fullpath)
573
if f != '' and self._directory_is_tree_reference(f):
574
kind = 'tree-reference'
578
if self._lock_mode != 'w':
579
raise errors.NotWriteLocked(self)
580
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
581
# already in use and GitFile doesn't allow overriding the lock file
583
f = open(self.control_transport.local_abspath('index'), 'wb')
584
# Note that _flush will close the file
590
write_index_dict(shaf, self.index)
592
except BaseException:
595
self._index_dirty = False
597
def has_or_had_id(self, file_id):
598
if self.has_id(file_id):
600
if self.had_id(file_id):
604
def had_id(self, file_id):
606
path = self.mapping.parse_file_id(file_id)
610
head = self.repository._git.head()
612
# Assume no if basis is not accessible
615
root_tree = self.store[head].tree
619
tree_lookup_path(self.store.__getitem__,
620
root_tree, path.encode('utf-8'))
626
def get_file_mtime(self, path):
627
"""See Tree.get_file_mtime."""
629
return self._lstat(path).st_mtime
631
if e.errno == errno.ENOENT:
632
raise errors.NoSuchFile(path)
635
def is_ignored(self, filename):
636
r"""Check whether the filename matches an ignore pattern.
638
If the file is ignored, returns the pattern which caused it to
639
be ignored, otherwise None. So this can simply be used as a
640
boolean if desired."""
641
if getattr(self, '_global_ignoreglobster', None) is None:
643
ignore_globs.update(ignores.get_runtime_ignores())
644
ignore_globs.update(ignores.get_user_ignores())
645
self._global_ignoreglobster = globbing.ExceptionGlobster(
647
match = self._global_ignoreglobster.match(filename)
648
if match is not None:
651
if self.kind(filename) == 'directory':
653
except errors.NoSuchFile:
655
filename = filename.lstrip('/')
656
ignore_manager = self._get_ignore_manager()
657
ps = list(ignore_manager.find_matching(filename))
660
if not ps[-1].is_exclude:
664
def _get_ignore_manager(self):
665
ignoremanager = getattr(self, '_ignoremanager', None)
666
if ignoremanager is not None:
669
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
670
self._ignoremanager = ignore_manager
671
return ignore_manager
673
def _flush_ignore_list_cache(self):
674
self._ignoremanager = None
676
def set_last_revision(self, revid):
677
if _mod_revision.is_null(revid):
678
self.branch.set_last_revision_info(0, revid)
680
_mod_revision.check_not_reserved_id(revid)
682
self.branch.generate_revision_history(revid)
683
except errors.NoSuchRevision:
684
raise errors.GhostRevisionUnusableHere(revid)
686
def _reset_data(self):
689
def get_file_verifier(self, path, stat_value=None):
690
with self.lock_read():
691
(index, subpath) = self._lookup_index(path.encode('utf-8'))
693
return ("GIT", index[subpath].sha)
695
if self._has_dir(path):
697
raise errors.NoSuchFile(path)
699
def get_file_sha1(self, path, stat_value=None):
700
with self.lock_read():
701
if not self.is_versioned(path):
702
raise errors.NoSuchFile(path)
703
abspath = self.abspath(path)
705
return osutils.sha_file_by_name(abspath)
707
if e.errno in (errno.EISDIR, errno.ENOENT):
711
def revision_tree(self, revid):
712
return self.repository.revision_tree(revid)
714
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
715
mode = stat_result.st_mode
716
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
718
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
719
return self.basis_tree().is_executable(path)
721
def stored_kind(self, path):
722
with self.lock_read():
723
encoded_path = path.encode('utf-8')
724
(index, subpath) = self._lookup_index(encoded_path)
726
return mode_kind(index[subpath].mode)
728
# Maybe it's a directory?
729
if self._has_dir(encoded_path):
731
raise errors.NoSuchFile(path)
733
def _lstat(self, path):
734
return os.lstat(self.abspath(path))
736
def _live_entry(self, path):
737
encoded_path = self.abspath(path.decode('utf-8')).encode(
739
return index_entry_from_path(encoded_path)
741
def is_executable(self, path):
742
with self.lock_read():
743
if self._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 self._supports_executable():
755
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
757
return self._is_executable_from_path_and_stat_from_basis(
760
def list_files(self, include_root=False, from_dir=None, recursive=True):
761
if from_dir is None or from_dir == '.':
764
fk_entries = {'directory': tree.TreeDirectory,
765
'file': tree.TreeFile,
766
'symlink': tree.TreeLink,
767
'tree-reference': tree.TreeReference}
768
with self.lock_read():
769
root_ie = self._get_dir_ie(u"", None)
770
if include_root and not from_dir:
771
yield "", "V", root_ie.kind, root_ie
772
dir_ids[u""] = root_ie.file_id
774
path_iterator = sorted(
775
self._iter_files_recursive(from_dir, include_dirs=True))
777
encoded_from_dir = self.abspath(from_dir).encode(
779
path_iterator = sorted(
780
[os.path.join(from_dir, name.decode(osutils._fs_enc))
781
for name in os.listdir(encoded_from_dir)
782
if not self.controldir.is_control_filename(
783
name.decode(osutils._fs_enc)) and
784
not self.mapping.is_special_file(
785
name.decode(osutils._fs_enc))])
786
for path in path_iterator:
788
encoded_path = path.encode("utf-8")
789
except UnicodeEncodeError:
790
raise errors.BadFilenameEncoding(
791
path, osutils._fs_enc)
792
(index, index_path) = self._lookup_index(encoded_path)
794
value = index[index_path]
797
kind = self.kind(path)
798
parent, name = posixpath.split(path)
799
for dir_path, dir_ie in self._add_missing_parent_ids(
802
if kind in ('directory', 'tree-reference'):
804
if self._has_dir(encoded_path):
805
ie = self._get_dir_ie(path, self.path2id(path))
807
elif self.is_ignored(path):
809
ie = fk_entries[kind]()
812
ie = fk_entries[kind]()
813
yield (posixpath.relpath(path, from_dir), status, kind,
816
if value is not None:
817
ie = self._get_file_ie(name, path, value, dir_ids[parent])
818
yield (posixpath.relpath(path, from_dir), "V", ie.kind, ie)
820
ie = fk_entries[kind]()
821
yield (posixpath.relpath(path, from_dir),
822
("I" if self.is_ignored(path) else "?"), kind, ie)
824
def all_file_ids(self):
825
raise errors.UnsupportedOperation(self.all_file_ids, self)
827
def all_versioned_paths(self):
828
with self.lock_read():
830
for path in self.index:
831
if self.mapping.is_special_file(path):
833
path = path.decode("utf-8")
836
path = posixpath.dirname(path).strip("/")
842
def iter_child_entries(self, path):
843
encoded_path = path.encode('utf-8')
844
with self.lock_read():
845
parent_id = self.path2id(path)
847
for item_path, value in self.index.iteritems():
848
decoded_item_path = item_path.decode('utf-8')
849
if self.mapping.is_special_file(item_path):
851
if not osutils.is_inside(path, decoded_item_path):
854
subpath = posixpath.relpath(decoded_item_path, path)
856
dirname = subpath.split('/', 1)[0]
857
file_ie = self._get_dir_ie(
858
posixpath.join(path, dirname), parent_id)
860
(unused_parent, name) = posixpath.split(decoded_item_path)
861
file_ie = self._get_file_ie(
862
name, decoded_item_path, value, parent_id)
864
if not found_any and path != u'':
865
raise errors.NoSuchFile(path)
868
with self.lock_read():
869
conflicts = _mod_conflicts.ConflictList()
870
for item_path, value in self.index.iteritems():
871
if value.flags & FLAG_STAGEMASK:
872
conflicts.append(_mod_conflicts.TextConflict(
873
item_path.decode('utf-8')))
876
def set_conflicts(self, conflicts):
878
for conflict in conflicts:
879
if conflict.typestring in ('text conflict', 'contents conflict'):
880
by_path.add(conflict.path.encode('utf-8'))
882
raise errors.UnsupportedOperation(self.set_conflicts, self)
883
with self.lock_tree_write():
884
for path in self.index:
885
self._set_conflicted(path, path in by_path)
887
def _set_conflicted(self, path, conflicted):
888
trace.mutter('change conflict: %r -> %r', path, conflicted)
889
value = self.index[path]
890
self._index_dirty = True
892
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
894
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
896
def add_conflicts(self, new_conflicts):
897
with self.lock_tree_write():
898
for conflict in new_conflicts:
899
if conflict.typestring in ('text conflict',
900
'contents conflict'):
902
self._set_conflicted(
903
conflict.path.encode('utf-8'), True)
905
raise errors.UnsupportedOperation(
906
self.add_conflicts, self)
908
raise errors.UnsupportedOperation(self.add_conflicts, self)
910
def walkdirs(self, prefix=""):
911
"""Walk the directories of this tree.
913
returns a generator which yields items in the form:
914
((curren_directory_path, fileid),
915
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
918
This API returns a generator, which is only valid during the current
919
tree transaction - within a single lock_read or lock_write duration.
921
If the tree is not locked, it may cause an error to be raised,
922
depending on the tree implementation.
924
from bisect import bisect_left
926
disk_top = self.abspath(prefix)
927
if disk_top.endswith('/'):
928
disk_top = disk_top[:-1]
929
top_strip_len = len(disk_top) + 1
930
inventory_iterator = self._walkdirs(prefix)
931
disk_iterator = osutils.walkdirs(disk_top, prefix)
933
current_disk = next(disk_iterator)
934
disk_finished = False
936
if not (e.errno == errno.ENOENT
937
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
942
current_inv = next(inventory_iterator)
944
except StopIteration:
947
while not inv_finished or not disk_finished:
949
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
950
cur_disk_dir_content) = current_disk
952
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
953
cur_disk_dir_content) = ((None, None), None)
954
if not disk_finished:
955
# strip out .bzr dirs
956
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
957
and len(cur_disk_dir_content) > 0):
958
# osutils.walkdirs can be made nicer -
959
# yield the path-from-prefix rather than the pathjoined
961
bzrdir_loc = bisect_left(cur_disk_dir_content,
963
if (bzrdir_loc < len(cur_disk_dir_content) and
964
self.controldir.is_control_filename(
965
cur_disk_dir_content[bzrdir_loc][0])):
966
# we dont yield the contents of, or, .bzr itself.
967
del cur_disk_dir_content[bzrdir_loc]
969
# everything is unknown
972
# everything is missing
975
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
976
- (current_inv[0][0] < cur_disk_dir_relpath))
978
# disk is before inventory - unknown
979
dirblock = [(relpath, basename, kind, stat, None, None) for
980
relpath, basename, kind, stat, top_path in
981
cur_disk_dir_content]
982
yield (cur_disk_dir_relpath, None), dirblock
984
current_disk = next(disk_iterator)
985
except StopIteration:
988
# inventory is before disk - missing.
989
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
990
for relpath, basename, dkind, stat, fileid, kind in
992
yield (current_inv[0][0], current_inv[0][1]), dirblock
994
current_inv = next(inventory_iterator)
995
except StopIteration:
998
# versioned present directory
999
# merge the inventory and disk data together
1001
for relpath, subiterator in itertools.groupby(sorted(
1002
current_inv[1] + cur_disk_dir_content,
1003
key=operator.itemgetter(0)), operator.itemgetter(1)):
1004
path_elements = list(subiterator)
1005
if len(path_elements) == 2:
1006
inv_row, disk_row = path_elements
1007
# versioned, present file
1008
dirblock.append((inv_row[0],
1009
inv_row[1], disk_row[2],
1010
disk_row[3], inv_row[4],
1012
elif len(path_elements[0]) == 5:
1015
(path_elements[0][0], path_elements[0][1],
1016
path_elements[0][2], path_elements[0][3],
1018
elif len(path_elements[0]) == 6:
1019
# versioned, absent file.
1021
(path_elements[0][0], path_elements[0][1],
1022
'unknown', None, path_elements[0][4],
1023
path_elements[0][5]))
1025
raise NotImplementedError('unreachable code')
1026
yield current_inv[0], dirblock
1028
current_inv = next(inventory_iterator)
1029
except StopIteration:
1032
current_disk = next(disk_iterator)
1033
except StopIteration:
1034
disk_finished = True
1036
def _walkdirs(self, prefix=u""):
1039
prefix = prefix.encode('utf-8')
1040
per_dir = defaultdict(set)
1042
per_dir[(u'', self.get_root_id())] = set()
1044
def add_entry(path, kind):
1045
if path == b'' or not path.startswith(prefix):
1047
(dirname, child_name) = posixpath.split(path)
1048
add_entry(dirname, 'directory')
1049
dirname = dirname.decode("utf-8")
1050
dir_file_id = self.path2id(dirname)
1051
if not isinstance(value, tuple) or len(value) != 10:
1052
raise ValueError(value)
1053
per_dir[(dirname, dir_file_id)].add(
1054
(path.decode("utf-8"), child_name.decode("utf-8"),
1056
self.path2id(path.decode("utf-8")),
1058
with self.lock_read():
1059
for path, value in self.index.iteritems():
1060
if self.mapping.is_special_file(path):
1062
if not path.startswith(prefix):
1064
add_entry(path, mode_kind(value.mode))
1065
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1067
def get_shelf_manager(self):
1068
raise workingtree.ShelvingUnsupported()
1070
def store_uncommitted(self):
1071
raise errors.StoringUncommittedNotSupported(self)
1073
def apply_inventory_delta(self, changes):
1074
for (old_path, new_path, file_id, ie) in changes:
1075
if old_path is not None:
1076
(index, old_subpath) = self._lookup_index(
1077
old_path.encode('utf-8'))
1079
self._index_del_entry(index, old_subpath)
1083
self._versioned_dirs = None
1084
if new_path is not None and ie.kind != 'directory':
1085
if ie.kind == 'tree-reference':
1086
self._index_add_entry(
1088
reference_revision=ie.reference_revision)
1090
self._index_add_entry(new_path, ie.kind)
1093
def annotate_iter(self, path,
1094
default_revision=_mod_revision.CURRENT_REVISION):
1095
"""See Tree.annotate_iter
1097
This implementation will use the basis tree implementation if possible.
1098
Lines not in the basis are attributed to CURRENT_REVISION
1100
If there are pending merges, lines added by those merges will be
1101
incorrectly attributed to CURRENT_REVISION (but after committing, the
1102
attribution will be correct).
1104
with self.lock_read():
1105
maybe_file_parent_keys = []
1106
for parent_id in self.get_parent_ids():
1108
parent_tree = self.revision_tree(parent_id)
1109
except errors.NoSuchRevisionInTree:
1110
parent_tree = self.branch.repository.revision_tree(
1112
with parent_tree.lock_read():
1113
# TODO(jelmer): Use rename/copy tracker to find path name
1117
kind = parent_tree.kind(parent_path)
1118
except errors.NoSuchFile:
1121
# Note: this is slightly unnecessary, because symlinks
1122
# and directories have a "text" which is the empty
1123
# text, and we know that won't mess up annotations. But
1128
parent_tree.get_file_revision(parent_path))
1129
if parent_text_key not in maybe_file_parent_keys:
1130
maybe_file_parent_keys.append(parent_text_key)
1131
# Now we have the parents of this content
1132
from breezy.annotate import Annotator
1133
from .annotate import AnnotateProvider
1134
annotate_provider = AnnotateProvider(
1135
self.branch.repository._file_change_scanner)
1136
annotator = Annotator(annotate_provider)
1138
from breezy.graph import Graph
1139
graph = Graph(annotate_provider)
1140
heads = graph.heads(maybe_file_parent_keys)
1141
file_parent_keys = []
1142
for key in maybe_file_parent_keys:
1144
file_parent_keys.append(key)
1146
text = self.get_file_text(path)
1147
this_key = (path, default_revision)
1148
annotator.add_special_text(this_key, file_parent_keys, text)
1149
annotations = [(key[-1], line)
1150
for key, line in annotator.annotate_flat(this_key)]
1153
def _rename_one(self, from_rel, to_rel):
1154
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1156
def _build_checkout_with_index(self):
1157
build_index_from_tree(
1158
self.user_transport.local_abspath('.'),
1159
self.control_transport.local_abspath("index"),
1162
if self.branch.head is None
1163
else self.store[self.branch.head].tree,
1164
honor_filemode=self._supports_executable())
1166
def reset_state(self, revision_ids=None):
1167
"""Reset the state of the working tree.
1169
This does a hard-reset to a last-known-good state. This is a way to
1170
fix if something got corrupted (like the .git/index file)
1172
with self.lock_tree_write():
1173
if revision_ids is not None:
1174
self.set_parent_ids(revision_ids)
1176
self._index_dirty = True
1177
if self.branch.head is not None:
1178
for entry in self.store.iter_tree_contents(
1179
self.store[self.branch.head].tree):
1180
if not validate_path(entry.path):
1183
if S_ISGITLINK(entry.mode):
1184
pass # TODO(jelmer): record and return submodule paths
1186
# Let's at least try to use the working tree file:
1188
st = self._lstat(self.abspath(
1189
entry.path.decode('utf-8')))
1191
# But if it doesn't exist, we'll make something up.
1192
obj = self.store[entry.sha]
1193
st = os.stat_result((entry.mode, 0, 0, 0,
1195
obj.as_raw_string()), 0,
1197
(index, subpath) = self._lookup_index(entry.path)
1198
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1200
def _update_git_tree(self, old_revision, new_revision, change_reporter=None,
1202
basis_tree = self.revision_tree(old_revision)
1203
if new_revision != old_revision:
1204
with basis_tree.lock_read():
1205
new_basis_tree = self.branch.basis_tree()
1211
change_reporter=change_reporter,
1212
show_base=show_base)
1214
def pull(self, source, overwrite=False, stop_revision=None,
1215
change_reporter=None, possible_transports=None, local=False,
1217
with self.lock_write(), source.lock_read():
1218
old_revision = self.branch.last_revision()
1219
count = self.branch.pull(source, overwrite, stop_revision,
1220
possible_transports=possible_transports,
1222
self._update_git_tree(
1223
old_revision=old_revision,
1224
new_revision=self.branch.last_revision(),
1225
change_reporter=change_reporter,
1226
show_base=show_base)
1229
def add_reference(self, sub_tree):
1230
"""Add a TreeReference to the tree, pointing at sub_tree.
1232
:param sub_tree: subtree to add.
1234
with self.lock_tree_write():
1236
sub_tree_path = self.relpath(sub_tree.basedir)
1237
except errors.PathNotChild:
1238
raise BadReferenceTarget(
1239
self, sub_tree, 'Target not inside tree.')
1241
self._add([sub_tree_path], [None], ['tree-reference'])
1243
def _read_submodule_head(self, path):
1244
return read_submodule_head(self.abspath(path))
1246
def get_reference_revision(self, path):
1247
hexsha = self._read_submodule_head(path)
1249
return _mod_revision.NULL_REVISION
1250
return self.branch.lookup_foreign_revision_id(hexsha)
1252
def get_nested_tree(self, path):
1253
return workingtree.WorkingTree.open(self.abspath(path))
1255
def _directory_is_tree_reference(self, relpath):
1256
# as a special case, if a directory contains control files then
1257
# it's a tree reference, except that the root of the tree is not
1258
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1260
def extract(self, sub_path, format=None):
1261
"""Extract a subtree from this tree.
1263
A new branch will be created, relative to the path for this tree.
1266
segments = osutils.splitpath(path)
1267
transport = self.branch.controldir.root_transport
1268
for name in segments:
1269
transport = transport.clone(name)
1270
transport.ensure_base()
1273
with self.lock_tree_write():
1275
branch_transport = mkdirs(sub_path)
1277
format = self.controldir.cloning_metadir()
1278
branch_transport.ensure_base()
1279
branch_bzrdir = format.initialize_on_transport(branch_transport)
1281
repo = branch_bzrdir.find_repository()
1282
except errors.NoRepositoryPresent:
1283
repo = branch_bzrdir.create_repository()
1284
if not repo.supports_rich_root():
1285
raise errors.RootNotRich()
1286
new_branch = branch_bzrdir.create_branch()
1287
new_branch.pull(self.branch)
1288
for parent_id in self.get_parent_ids():
1289
new_branch.fetch(self.branch, parent_id)
1290
tree_transport = self.controldir.root_transport.clone(sub_path)
1291
if tree_transport.base != branch_transport.base:
1292
tree_bzrdir = format.initialize_on_transport(tree_transport)
1293
tree_bzrdir.set_branch_reference(new_branch)
1295
tree_bzrdir = branch_bzrdir
1296
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1297
wt.set_parent_ids(self.get_parent_ids())
1300
def _get_check_refs(self):
1301
"""Return the references needed to perform a check of this tree.
1303
The default implementation returns no refs, and is only suitable for
1304
trees that have no local caching and can commit on ghosts at any time.
1306
:seealso: breezy.check for details about check_refs.
1310
def copy_content_into(self, tree, revision_id=None):
1311
"""Copy the current content and user files of this tree into tree."""
1312
with self.lock_read():
1313
if revision_id is None:
1314
merge.transform_tree(tree, self)
1316
# TODO now merge from tree.last_revision to revision (to
1317
# preserve user local changes)
1319
other_tree = self.revision_tree(revision_id)
1320
except errors.NoSuchRevision:
1321
other_tree = self.branch.repository.revision_tree(
1324
merge.transform_tree(tree, other_tree)
1325
if revision_id == _mod_revision.NULL_REVISION:
1328
new_parents = [revision_id]
1329
tree.set_parent_ids(new_parents)
1332
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1334
_tree_class = GitWorkingTree
1336
supports_versioned_directories = False
1338
supports_setting_file_ids = False
1340
supports_store_uncommitted = False
1342
supports_leftmost_parent_id_as_ghost = False
1344
supports_righthand_parent_id_as_ghost = False
1346
requires_normalized_unicode_filenames = True
1348
supports_merge_modified = False
1350
ignore_filename = ".gitignore"
1353
def _matchingcontroldir(self):
1354
from .dir import LocalGitControlDirFormat
1355
return LocalGitControlDirFormat()
1357
def get_format_description(self):
1358
return "Git Working Tree"
1360
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1361
accelerator_tree=None, hardlink=False):
1362
"""See WorkingTreeFormat.initialize()."""
1363
if not isinstance(a_controldir, LocalGitDir):
1364
raise errors.IncompatibleFormat(self, a_controldir)
1365
branch = a_controldir.open_branch(nascent_ok=True)
1366
if revision_id is not None:
1367
branch.set_last_revision(revision_id)
1368
wt = GitWorkingTree(
1369
a_controldir, a_controldir.open_repository(), branch)
1370
for hook in MutableTree.hooks['post_build_tree']: