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
IGNORE_FILENAME = ".gitignore"
89
class GitWorkingTree(MutableGitIndexTree,workingtree.WorkingTree):
90
"""A Git working tree."""
92
def __init__(self, controldir, repo, branch):
93
MutableGitIndexTree.__init__(self)
94
basedir = controldir.root_transport.local_abspath('.')
95
self.basedir = osutils.realpath(basedir)
96
self.controldir = controldir
97
self.repository = repo
98
self.store = self.repository._git.object_store
99
self.mapping = self.repository.get_mapping()
100
self._branch = branch
101
self._transport = self.repository._git._controltransport
102
self._format = GitWorkingTreeFormat()
104
self._index_file = None
105
self.views = self._make_views()
106
self._rules_searcher = None
107
self._detect_case_handling()
110
def supports_tree_reference(self):
113
def supports_rename_tracking(self):
116
def _read_index(self):
117
self.index = Index(self.control_transport.local_abspath('index'))
118
self._index_dirty = False
121
"""Lock the repository for read operations.
123
:return: A breezy.lock.LogicalLockResult.
125
if not self._lock_mode:
126
self._lock_mode = 'r'
130
self._lock_count += 1
131
self.branch.lock_read()
132
return lock.LogicalLockResult(self.unlock)
134
def _lock_write_tree(self):
135
if not self._lock_mode:
136
self._lock_mode = 'w'
139
self._index_file = GitFile(self.control_transport.local_abspath('index'), 'wb')
141
raise errors.LockContention('index')
143
elif self._lock_mode == 'r':
144
raise errors.ReadOnlyError(self)
148
def lock_tree_write(self):
149
self.branch.lock_read()
151
self._lock_write_tree()
152
return lock.LogicalLockResult(self.unlock)
157
def lock_write(self, token=None):
158
self.branch.lock_write()
160
self._lock_write_tree()
161
return lock.LogicalLockResult(self.unlock)
167
return self._lock_count >= 1
169
def get_physical_lock_status(self):
172
def break_lock(self):
174
self.control_transport.delete('index.lock')
175
except errors.NoSuchFile:
177
self.branch.break_lock()
179
@only_raises(errors.LockNotHeld, errors.LockBroken)
181
if not self._lock_count:
182
return lock.cant_unlock_not_held(self)
185
self._lock_count -= 1
186
if self._lock_count > 0:
188
if self._index_file is not None:
189
if self._index_dirty:
190
self._flush(self._index_file)
191
self._index_file.close()
193
# Something else already triggered a write of the index
194
# file by calling .flush()
195
self._index_file.abort()
196
self._index_file = None
197
self._lock_mode = None
205
def _detect_case_handling(self):
207
self._transport.stat(".git/cOnFiG")
208
except errors.NoSuchFile:
209
self.case_sensitive = True
211
self.case_sensitive = False
213
def merge_modified(self):
216
def set_merge_modified(self, modified_hashes):
217
raise errors.UnsupportedOperation(self.set_merge_modified, self)
219
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
220
self.set_parent_ids([p for p, t in parents_list])
222
def _set_merges_from_parent_ids(self, rhs_parent_ids):
224
merges = [self.branch.lookup_bzr_revision_id(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('MERGE_HEAD', b'\n'.join(merges),
229
mode=self.controldir._get_file_mode())
232
self.control_transport.delete('MERGE_HEAD')
233
except errors.NoSuchFile:
236
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
237
"""Set the parent ids to revision_ids.
239
See also set_parent_trees. This api will try to retrieve the tree data
240
for each element of revision_ids from the trees repository. If you have
241
tree data already available, it is more efficient to use
242
set_parent_trees rather than set_parent_ids. set_parent_ids is however
243
an easier API to use.
245
:param revision_ids: The revision_ids to set as the parent ids of this
246
working tree. Any of these may be ghosts.
248
with self.lock_tree_write():
249
self._check_parents_for_ghosts(revision_ids,
250
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
251
for revision_id in revision_ids:
252
_mod_revision.check_not_reserved_id(revision_id)
254
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
256
if len(revision_ids) > 0:
257
self.set_last_revision(revision_ids[0])
259
self.set_last_revision(_mod_revision.NULL_REVISION)
261
self._set_merges_from_parent_ids(revision_ids[1:])
263
def get_parent_ids(self):
264
"""See Tree.get_parent_ids.
266
This implementation reads the pending merges list and last_revision
267
value and uses that to decide what the parents list should be.
269
last_rev = _mod_revision.ensure_null(self._last_revision())
270
if _mod_revision.NULL_REVISION == last_rev:
275
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
276
except errors.NoSuchFile:
279
for l in osutils.split_lines(merges_bytes):
280
revision_id = l.rstrip(b'\n')
281
parents.append(self.branch.lookup_foreign_revision_id(revision_id))
284
def check_state(self):
285
"""Check that the working state is/isn't valid."""
288
def remove(self, files, verbose=False, to_file=None, keep_files=True,
290
"""Remove nominated files from the working tree metadata.
292
:param files: File paths relative to the basedir.
293
:param keep_files: If true, the files will also be kept.
294
:param force: Delete files and directories, even if they are changed
295
and even if the directories are not empty.
297
if not isinstance(files, list):
303
def backup(file_to_backup):
304
abs_path = self.abspath(file_to_backup)
305
backup_name = self.controldir._available_backup_name(file_to_backup)
306
osutils.rename(abs_path, self.abspath(backup_name))
307
return "removed %s (but kept a copy: %s)" % (
308
file_to_backup, backup_name)
310
# Sort needed to first handle directory content before the directory
315
def recurse_directory_to_add_files(directory):
316
# Recurse directory and add all files
317
# so we can check if they have changed.
318
for parent_info, file_infos in self.walkdirs(directory):
319
for relpath, basename, kind, lstat, fileid, kind in file_infos:
320
# Is it versioned or ignored?
321
if self.is_versioned(relpath):
322
# Add nested content for deletion.
323
all_files.add(relpath)
325
# Files which are not versioned
326
# should be treated as unknown.
327
files_to_backup.append(relpath)
329
with self.lock_tree_write():
330
for filepath in files:
331
# Get file name into canonical form.
332
abspath = self.abspath(filepath)
333
filepath = self.relpath(abspath)
336
all_files.add(filepath)
337
recurse_directory_to_add_files(filepath)
339
files = list(all_files)
342
return # nothing to do
344
# Sort needed to first handle directory content before the directory
345
files.sort(reverse=True)
347
# Bail out if we are going to delete files we shouldn't
348
if not keep_files and not force:
349
for (file_id, path, content_change, versioned, parent_id, name,
350
kind, executable) in self.iter_changes(self.basis_tree(),
351
include_unchanged=True, require_versioned=False,
352
want_unversioned=True, specific_files=files):
353
if versioned[0] == False:
354
# The record is unknown or newly added
355
files_to_backup.append(path[1])
356
files_to_backup.extend(osutils.parent_directories(path[1]))
357
elif (content_change and (kind[1] is not None) and
358
osutils.is_inside_any(files, path[1])):
359
# Versioned and changed, but not deleted, and still
360
# in one of the dirs to be deleted.
361
files_to_backup.append(path[1])
362
files_to_backup.extend(osutils.parent_directories(path[1]))
370
except errors.NoSuchFile:
373
abs_path = self.abspath(f)
375
# having removed it, it must be either ignored or unknown
376
if self.is_ignored(f):
380
kind_ch = osutils.kind_marker(kind)
381
to_file.write(new_status + ' ' + f + kind_ch + '\n')
383
message = "%s does not exist" % (f, )
386
if f in files_to_backup and not force:
389
if kind == 'directory':
390
osutils.rmtree(abs_path)
392
osutils.delete_any(abs_path)
393
message = "deleted %s" % (f,)
395
message = "removed %s" % (f,)
396
self._unversion_path(f)
398
# print only one message (if any) per file.
399
if message is not None:
401
self._versioned_dirs = None
403
def smart_add(self, file_list, recurse=True, action=None, save=True):
407
# expand any symlinks in the directory part, while leaving the
409
# only expanding if symlinks are supported avoids windows path bugs
410
if osutils.has_symlinks():
411
file_list = list(map(osutils.normalizepath, file_list))
413
conflicts_related = set()
414
for c in self.conflicts():
415
conflicts_related.update(c.associated_filenames())
420
def call_action(filepath, kind):
423
if action is not None:
424
parent_path = posixpath.dirname(filepath)
425
parent_id = self.path2id(parent_path)
426
parent_ie = self._get_dir_ie(parent_path, parent_id)
427
file_id = action(self, parent_ie, filepath, kind)
428
if file_id is not None:
429
raise workingtree.SettingFileIdUnsupported()
431
with self.lock_tree_write():
432
for filepath in osutils.canonical_relpaths(self.basedir, file_list):
433
filepath, can_access = osutils.normalized_filename(filepath)
435
raise errors.InvalidNormalization(filepath)
437
abspath = self.abspath(filepath)
438
kind = osutils.file_kind(abspath)
439
if kind in ("file", "symlink"):
440
(index, subpath) = self._lookup_index(filepath.encode('utf-8'))
444
call_action(filepath, kind)
446
self._index_add_entry(filepath, kind)
447
added.append(filepath)
448
elif kind == "directory":
449
(index, subpath) = self._lookup_index(filepath.encode('utf-8'))
450
if subpath not in index:
451
call_action(filepath, kind)
453
user_dirs.append(filepath)
455
raise errors.BadFileKindError(filename=abspath, kind=kind)
456
for user_dir in user_dirs:
457
abs_user_dir = self.abspath(user_dir)
460
transport = _mod_transport.get_transport_from_path(abs_user_dir)
461
_mod_controldir.ControlDirFormat.find_format(transport)
463
except errors.NotBranchError:
465
except errors.UnsupportedFormatError:
470
trace.warning('skipping nested tree %r', abs_user_dir)
473
for name in os.listdir(abs_user_dir):
474
subp = os.path.join(user_dir, name)
475
if self.is_control_filename(subp) or self.mapping.is_special_file(subp):
477
ignore_glob = self.is_ignored(subp)
478
if ignore_glob is not None:
479
ignored.setdefault(ignore_glob, []).append(subp)
481
abspath = self.abspath(subp)
482
kind = osutils.file_kind(abspath)
483
if kind == "directory":
484
user_dirs.append(subp)
486
(index, subpath) = self._lookup_index(subp.encode('utf-8'))
490
if subp in conflicts_related:
492
call_action(subp, kind)
494
self._index_add_entry(subp, kind)
496
return added, ignored
498
def has_filename(self, filename):
499
return osutils.lexists(self.abspath(filename))
501
def _iter_files_recursive(self, from_dir=None, include_dirs=False):
504
for (dirpath, dirnames, filenames) in os.walk(self.abspath(from_dir).encode(osutils._fs_enc)):
505
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
506
if self.controldir.is_control_filename(dir_relpath.decode(osutils._fs_enc)):
508
for name in list(dirnames):
509
if self.controldir.is_control_filename(name.decode(osutils._fs_enc)):
510
dirnames.remove(name)
512
relpath = os.path.join(dir_relpath, name)
515
yield relpath.decode(osutils._fs_enc)
516
except UnicodeDecodeError:
517
raise errors.BadFilenameEncoding(
518
relpath, osutils._fs_enc)
519
if not self._has_dir(relpath):
520
dirnames.remove(name)
521
for name in filenames:
522
if not self.mapping.is_special_file(name):
523
yp = os.path.join(dir_relpath, name)
525
yield yp.decode(osutils._fs_enc)
526
except UnicodeDecodeError:
527
raise errors.BadFilenameEncoding(
531
"""Yield all unversioned files in this WorkingTree.
533
with self.lock_read():
534
index_paths = set([p.decode('utf-8') for p, i in self._recurse_index_entries()])
535
all_paths = set(self._iter_files_recursive(include_dirs=True))
536
for p in (all_paths - index_paths):
537
if not self._has_dir(p.encode('utf-8')):
540
def _gather_kinds(self, files, kinds):
541
"""See MutableTree._gather_kinds."""
542
with self.lock_tree_write():
543
for pos, f in enumerate(files):
544
if kinds[pos] is None:
545
fullpath = osutils.normpath(self.abspath(f))
547
kind = osutils.file_kind(fullpath)
549
if e.errno == errno.ENOENT:
550
raise errors.NoSuchFile(fullpath)
551
if kind == 'directory' and f != '' and os.path.exists(os.path.join(fullpath, '.git')):
552
kind = 'tree-reference'
556
if self._lock_mode != 'w':
557
raise errors.NotWriteLocked(self)
558
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
559
# already in use and GitFile doesn't allow overriding the lock file name :(
560
f = open(self.control_transport.local_abspath('index'), 'wb')
561
# Note that _flush will close the file
567
write_index_dict(shaf, self.index)
572
self._index_dirty = False
574
def has_or_had_id(self, file_id):
575
if self.has_id(file_id):
577
if self.had_id(file_id):
581
def had_id(self, file_id):
582
path = self._basis_fileid_map.lookup_path(file_id)
584
head = self.repository._git.head()
586
# Assume no if basis is not accessible
589
root_tree = self.store[head].tree
593
tree_lookup_path(self.store.__getitem__, root_tree, path.encode('utf-8'))
599
def get_file_mtime(self, path, file_id=None):
600
"""See Tree.get_file_mtime."""
602
return self._lstat(path).st_mtime
604
if e.errno == errno.ENOENT:
605
raise errors.NoSuchFile(path)
608
def is_ignored(self, filename):
609
r"""Check whether the filename matches an ignore pattern.
611
If the file is ignored, returns the pattern which caused it to
612
be ignored, otherwise None. So this can simply be used as a
613
boolean if desired."""
614
if getattr(self, '_global_ignoreglobster', None) is None:
616
ignore_globs.update(ignores.get_runtime_ignores())
617
ignore_globs.update(ignores.get_user_ignores())
618
self._global_ignoreglobster = globbing.ExceptionGlobster(ignore_globs)
619
match = self._global_ignoreglobster.match(filename)
620
if match is not None:
623
if self.kind(filename) == 'directory':
625
except errors.NoSuchFile:
627
filename = filename.lstrip('/')
628
ignore_manager = self._get_ignore_manager()
629
ps = list(ignore_manager.find_matching(filename))
632
if not ps[-1].is_exclude:
636
def _get_ignore_manager(self):
637
ignoremanager = getattr(self, '_ignoremanager', None)
638
if ignoremanager is not None:
641
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
642
self._ignoremanager = ignore_manager
643
return ignore_manager
645
def _flush_ignore_list_cache(self):
646
self._ignoremanager = None
648
def set_last_revision(self, revid):
649
if _mod_revision.is_null(revid):
650
self.branch.set_last_revision_info(0, revid)
652
_mod_revision.check_not_reserved_id(revid)
654
self.branch.generate_revision_history(revid)
655
except errors.NoSuchRevision:
656
raise errors.GhostRevisionUnusableHere(revid)
658
def _reset_data(self):
660
head = self.repository._git.head()
662
self._basis_fileid_map = GitFileIdMap({}, self.mapping)
664
self._basis_fileid_map = self.mapping.get_fileid_map(
665
self.store.__getitem__, self.store[head].tree)
666
self._fileid_map = self._basis_fileid_map.copy()
668
def get_file_verifier(self, path, file_id=None, stat_value=None):
669
with self.lock_read():
670
(index, subpath) = self._lookup_index(path.encode('utf-8'))
672
return ("GIT", index[subpath].sha)
674
if self._has_dir(path):
676
raise errors.NoSuchFile(path)
678
def get_file_sha1(self, path, file_id=None, stat_value=None):
679
with self.lock_read():
680
if not self.is_versioned(path):
681
raise errors.NoSuchFile(path)
682
abspath = self.abspath(path)
684
return osutils.sha_file_by_name(abspath)
686
if e.errno in (errno.EISDIR, errno.ENOENT):
690
def revision_tree(self, revid):
691
return self.repository.revision_tree(revid)
693
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
694
mode = stat_result.st_mode
695
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
697
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
698
return self.basis_tree().is_executable(path)
700
def stored_kind(self, path, file_id=None):
701
with self.lock_read():
702
encoded_path = path.encode('utf-8')
703
(index, subpath) = self._lookup_index(encoded_path)
705
return mode_kind(index[subpath].mode)
707
# Maybe it's a directory?
708
if self._has_dir(encoded_path):
710
raise errors.NoSuchFile(path)
712
def _lstat(self, path):
713
return os.lstat(self.abspath(path))
715
def _live_entry(self, path):
716
return index_entry_from_path(self.abspath(path.decode('utf-8')).encode(osutils._fs_enc))
718
def is_executable(self, path, file_id=None):
719
with self.lock_read():
720
if getattr(self, "_supports_executable", osutils.supports_executable)():
721
mode = self._lstat(path).st_mode
723
(index, subpath) = self._lookup_index(path.encode('utf-8'))
725
mode = index[subpath].mode
728
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
730
def _is_executable_from_path_and_stat(self, path, stat_result):
731
if getattr(self, "_supports_executable", osutils.supports_executable)():
732
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
734
return self._is_executable_from_path_and_stat_from_basis(path, stat_result)
736
def list_files(self, include_root=False, from_dir=None, recursive=True):
740
fk_entries = {'directory': tree.TreeDirectory,
741
'file': tree.TreeFile,
742
'symlink': tree.TreeLink,
743
'tree-reference': tree.TreeReference}
744
with self.lock_read():
745
root_ie = self._get_dir_ie(u"", None)
746
if include_root and not from_dir:
747
yield "", "V", root_ie.kind, root_ie.file_id, root_ie
748
dir_ids[u""] = root_ie.file_id
750
path_iterator = sorted(self._iter_files_recursive(from_dir, include_dirs=True))
752
path_iterator = sorted([os.path.join(from_dir, name.decode(osutils._fs_enc)) for name in
753
os.listdir(self.abspath(from_dir).encode(osutils._fs_enc))
754
if not self.controldir.is_control_filename(name.decode(osutils._fs_enc))
755
and not self.mapping.is_special_file(name.decode(osutils._fs_enc))])
756
for path in path_iterator:
758
encoded_path = path.encode("utf-8")
759
except UnicodeEncodeError:
760
raise errors.BadFilenameEncoding(
761
path, osutils._fs_enc)
762
(index, index_path) = self._lookup_index(encoded_path)
764
value = index[index_path]
767
kind = self.kind(path)
768
parent, name = posixpath.split(path)
769
for dir_path, dir_ie in self._add_missing_parent_ids(parent, dir_ids):
771
if kind in ('directory', 'tree-reference'):
773
if self._has_dir(encoded_path):
774
ie = self._get_dir_ie(path, self.path2id(path))
777
elif self.is_ignored(path):
779
ie = fk_entries[kind]()
783
ie = fk_entries[kind]()
785
yield posixpath.relpath(path, from_dir), status, kind, file_id, ie
787
if value is not None:
788
ie = self._get_file_ie(name, path, value, dir_ids[parent])
789
yield posixpath.relpath(path, from_dir), "V", ie.kind, ie.file_id, ie
791
ie = fk_entries[kind]()
792
yield posixpath.relpath(path, from_dir), ("I" if self.is_ignored(path) else "?"), kind, None, ie
794
def all_file_ids(self):
795
with self.lock_read():
796
ids = {u"": self.path2id("")}
797
for path in self.index:
798
if self.mapping.is_special_file(path):
800
path = path.decode("utf-8")
801
parent = posixpath.dirname(path).strip("/")
802
for e in self._add_missing_parent_ids(parent, ids):
804
ids[path] = self.path2id(path)
805
return set(ids.values())
807
def all_versioned_paths(self):
808
with self.lock_read():
810
for path in self.index:
811
if self.mapping.is_special_file(path):
813
path = path.decode("utf-8")
816
path = posixpath.dirname(path).strip("/")
822
def iter_child_entries(self, path, file_id=None):
823
encoded_path = path.encode('utf-8')
824
with self.lock_read():
825
parent_id = self.path2id(path)
827
seen_children = set()
828
for item_path, value in self.index.iteritems():
829
decoded_item_path = item_path.decode('utf-8')
830
if self.mapping.is_special_file(item_path):
832
if not osutils.is_inside(path, decoded_item_path):
835
subpath = posixpath.relpath(decoded_item_path, path)
837
dirname = subpath.split('/', 1)[0]
838
file_ie = self._get_dir_ie(posixpath.join(path, dirname), parent_id)
840
(unused_parent, name) = posixpath.split(decoded_item_path)
841
file_ie = self._get_file_ie(
842
name, decoded_item_path, value, parent_id)
844
if not found_any and path != u'':
845
raise errors.NoSuchFile(path)
848
with self.lock_read():
849
conflicts = _mod_conflicts.ConflictList()
850
for item_path, value in self.index.iteritems():
851
if value.flags & FLAG_STAGEMASK:
852
conflicts.append(_mod_conflicts.TextConflict(item_path.decode('utf-8')))
855
def set_conflicts(self, conflicts):
857
for conflict in conflicts:
858
if conflict.typestring in ('text conflict', 'contents conflict'):
859
by_path.add(conflict.path.encode('utf-8'))
861
raise errors.UnsupportedOperation(self.set_conflicts, self)
862
with self.lock_tree_write():
863
for path in self.index:
864
self._set_conflicted(path, path in by_path)
866
def _set_conflicted(self, path, conflicted):
867
trace.mutter('change conflict: %r -> %r', path, conflicted)
868
value = self.index[path]
869
self._index_dirty = True
871
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
873
self.index[path] = (value[:9] + (value[9] &~ FLAG_STAGEMASK, ))
875
def add_conflicts(self, new_conflicts):
876
with self.lock_tree_write():
877
for conflict in new_conflicts:
878
if conflict.typestring in ('text conflict', 'contents conflict'):
880
self._set_conflicted(conflict.path.encode('utf-8'), True)
882
raise errors.UnsupportedOperation(self.add_conflicts, self)
884
raise errors.UnsupportedOperation(self.add_conflicts, self)
886
def walkdirs(self, prefix=""):
887
"""Walk the directories of this tree.
889
returns a generator which yields items in the form:
890
((curren_directory_path, fileid),
891
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
894
This API returns a generator, which is only valid during the current
895
tree transaction - within a single lock_read or lock_write duration.
897
If the tree is not locked, it may cause an error to be raised,
898
depending on the tree implementation.
900
from bisect import bisect_left
902
disk_top = self.abspath(prefix)
903
if disk_top.endswith('/'):
904
disk_top = disk_top[:-1]
905
top_strip_len = len(disk_top) + 1
906
inventory_iterator = self._walkdirs(prefix)
907
disk_iterator = osutils.walkdirs(disk_top, prefix)
909
current_disk = next(disk_iterator)
910
disk_finished = False
912
if not (e.errno == errno.ENOENT or
913
(sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
918
current_inv = next(inventory_iterator)
920
except StopIteration:
923
while not inv_finished or not disk_finished:
925
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
926
cur_disk_dir_content) = current_disk
928
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
929
cur_disk_dir_content) = ((None, None), None)
930
if not disk_finished:
931
# strip out .bzr dirs
932
if (cur_disk_dir_path_from_top[top_strip_len:] == '' and
933
len(cur_disk_dir_content) > 0):
934
# osutils.walkdirs can be made nicer -
935
# yield the path-from-prefix rather than the pathjoined
937
bzrdir_loc = bisect_left(cur_disk_dir_content,
939
if (bzrdir_loc < len(cur_disk_dir_content)
940
and self.controldir.is_control_filename(
941
cur_disk_dir_content[bzrdir_loc][0])):
942
# we dont yield the contents of, or, .bzr itself.
943
del cur_disk_dir_content[bzrdir_loc]
945
# everything is unknown
948
# everything is missing
951
direction = ((current_inv[0][0] > cur_disk_dir_relpath) -
952
(current_inv[0][0] < cur_disk_dir_relpath))
954
# disk is before inventory - unknown
955
dirblock = [(relpath, basename, kind, stat, None, None) for
956
relpath, basename, kind, stat, top_path in
957
cur_disk_dir_content]
958
yield (cur_disk_dir_relpath, None), dirblock
960
current_disk = next(disk_iterator)
961
except StopIteration:
964
# inventory is before disk - missing.
965
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
966
for relpath, basename, dkind, stat, fileid, kind in
968
yield (current_inv[0][0], current_inv[0][1]), dirblock
970
current_inv = next(inventory_iterator)
971
except StopIteration:
974
# versioned present directory
975
# merge the inventory and disk data together
977
for relpath, subiterator in itertools.groupby(sorted(
978
current_inv[1] + cur_disk_dir_content,
979
key=operator.itemgetter(0)), operator.itemgetter(1)):
980
path_elements = list(subiterator)
981
if len(path_elements) == 2:
982
inv_row, disk_row = path_elements
983
# versioned, present file
984
dirblock.append((inv_row[0],
985
inv_row[1], disk_row[2],
986
disk_row[3], inv_row[4],
988
elif len(path_elements[0]) == 5:
990
dirblock.append((path_elements[0][0],
991
path_elements[0][1], path_elements[0][2],
992
path_elements[0][3], None, None))
993
elif len(path_elements[0]) == 6:
994
# versioned, absent file.
995
dirblock.append((path_elements[0][0],
996
path_elements[0][1], 'unknown', None,
997
path_elements[0][4], path_elements[0][5]))
999
raise NotImplementedError('unreachable code')
1000
yield current_inv[0], dirblock
1002
current_inv = next(inventory_iterator)
1003
except StopIteration:
1006
current_disk = next(disk_iterator)
1007
except StopIteration:
1008
disk_finished = True
1010
def _walkdirs(self, prefix=u""):
1013
prefix = prefix.encode('utf-8')
1014
per_dir = defaultdict(set)
1016
per_dir[(u'', self.get_root_id())] = set()
1017
def add_entry(path, kind):
1018
if path == b'' or not path.startswith(prefix):
1020
(dirname, child_name) = posixpath.split(path)
1021
add_entry(dirname, 'directory')
1022
dirname = dirname.decode("utf-8")
1023
dir_file_id = self.path2id(dirname)
1024
if not isinstance(value, tuple) or len(value) != 10:
1025
raise ValueError(value)
1026
per_dir[(dirname, dir_file_id)].add(
1027
(path.decode("utf-8"), child_name.decode("utf-8"),
1029
self.path2id(path.decode("utf-8")),
1031
with self.lock_read():
1032
for path, value in self.index.iteritems():
1033
if self.mapping.is_special_file(path):
1035
if not path.startswith(prefix):
1037
add_entry(path, mode_kind(value.mode))
1038
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1040
def get_shelf_manager(self):
1041
raise workingtree.ShelvingUnsupported()
1043
def store_uncommitted(self):
1044
raise errors.StoringUncommittedNotSupported(self)
1046
def apply_inventory_delta(self, changes):
1047
for (old_path, new_path, file_id, ie) in changes:
1048
if old_path is not None:
1049
(index, old_subpath) = self._lookup_index(old_path.encode('utf-8'))
1051
self._index_del_entry(index, old_subpath)
1055
self._versioned_dirs = None
1056
if new_path is not None and ie.kind != 'directory':
1057
if ie.kind == 'tree-reference':
1058
self._index_add_entry(
1060
reference_revision=ie.reference_revision)
1062
self._index_add_entry(new_path, ie.kind)
1065
def annotate_iter(self, path, file_id=None,
1066
default_revision=_mod_revision.CURRENT_REVISION):
1067
"""See Tree.annotate_iter
1069
This implementation will use the basis tree implementation if possible.
1070
Lines not in the basis are attributed to CURRENT_REVISION
1072
If there are pending merges, lines added by those merges will be
1073
incorrectly attributed to CURRENT_REVISION (but after committing, the
1074
attribution will be correct).
1076
with self.lock_read():
1077
maybe_file_parent_keys = []
1078
for parent_id in self.get_parent_ids():
1080
parent_tree = self.revision_tree(parent_id)
1081
except errors.NoSuchRevisionInTree:
1082
parent_tree = self.branch.repository.revision_tree(
1084
with parent_tree.lock_read():
1085
# TODO(jelmer): Use rename/copy tracker to find path name in parent
1088
kind = parent_tree.kind(parent_path)
1089
except errors.NoSuchFile:
1092
# Note: this is slightly unnecessary, because symlinks and
1093
# directories have a "text" which is the empty text, and we
1094
# know that won't mess up annotations. But it seems cleaner
1098
parent_tree.get_file_revision(parent_path))
1099
if parent_text_key not in maybe_file_parent_keys:
1100
maybe_file_parent_keys.append(parent_text_key)
1101
# Now we have the parents of this content
1102
from breezy.annotate import Annotator
1103
from .annotate import AnnotateProvider
1104
annotate_provider = AnnotateProvider(
1105
self.branch.repository._file_change_scanner)
1106
annotator = Annotator(annotate_provider)
1108
from breezy.graph import Graph
1109
graph = Graph(annotate_provider)
1110
heads = graph.heads(maybe_file_parent_keys)
1111
file_parent_keys = []
1112
for key in maybe_file_parent_keys:
1114
file_parent_keys.append(key)
1116
text = self.get_file_text(path)
1117
this_key = (path, default_revision)
1118
annotator.add_special_text(this_key, file_parent_keys, text)
1119
annotations = [(key[-1], line)
1120
for key, line in annotator.annotate_flat(this_key)]
1123
def _rename_one(self, from_rel, to_rel):
1124
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1126
def _build_checkout_with_index(self):
1127
build_index_from_tree(
1128
self.user_transport.local_abspath('.'),
1129
self.control_transport.local_abspath("index"),
1131
None if self.branch.head is None else self.store[self.branch.head].tree)
1133
def reset_state(self, revision_ids=None):
1134
"""Reset the state of the working tree.
1136
This does a hard-reset to a last-known-good state. This is a way to
1137
fix if something got corrupted (like the .git/index file)
1139
with self.lock_tree_write():
1140
if revision_ids is not None:
1141
self.set_parent_ids(revision_ids)
1143
self._index_dirty = True
1144
if self.branch.head is not None:
1145
for entry in self.store.iter_tree_contents(self.store[self.branch.head].tree):
1146
if not validate_path(entry.path):
1149
if S_ISGITLINK(entry.mode):
1150
pass # TODO(jelmer): record and return submodule paths
1152
# Let's at least try to use the working tree file:
1154
st = self._lstat(self.abspath(entry.path.decode('utf-8')))
1156
# But if it doesn't exist, we'll make something up.
1157
obj = self.store[entry.sha]
1158
st = os.stat_result((entry.mode, 0, 0, 0,
1159
0, 0, len(obj.as_raw_string()), 0,
1161
(index, subpath) = self._lookup_index(entry.path)
1162
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1164
def pull(self, source, overwrite=False, stop_revision=None,
1165
change_reporter=None, possible_transports=None, local=False,
1167
with self.lock_write(), source.lock_read():
1168
old_revision = self.branch.last_revision()
1169
basis_tree = self.basis_tree()
1170
count = self.branch.pull(source, overwrite, stop_revision,
1171
possible_transports=possible_transports,
1173
new_revision = self.branch.last_revision()
1174
if new_revision != old_revision:
1175
with basis_tree.lock_read():
1176
new_basis_tree = self.branch.basis_tree()
1182
change_reporter=change_reporter,
1183
show_base=show_base)
1186
def add_reference(self, sub_tree):
1187
"""Add a TreeReference to the tree, pointing at sub_tree.
1189
:param sub_tree: subtree to add.
1191
with self.lock_tree_write():
1193
sub_tree_path = self.relpath(sub_tree.basedir)
1194
except errors.PathNotChild:
1195
raise BadReferenceTarget(
1196
self, sub_tree, 'Target not inside tree.')
1198
self._add([sub_tree_path], [None], ['tree-reference'])
1200
def _read_submodule_head(self, path):
1201
return read_submodule_head(self.abspath(path))
1203
def get_reference_revision(self, path, file_id=None):
1204
hexsha = self._read_submodule_head(path)
1206
return _mod_revision.NULL_REVISION
1207
return self.branch.lookup_foreign_revision_id(hexsha)
1209
def get_nested_tree(self, path, file_id=None):
1210
return workingtree.WorkingTree.open(self.abspath(path))
1212
def _directory_is_tree_reference(self, relpath):
1213
# as a special case, if a directory contains control files then
1214
# it's a tree reference, except that the root of the tree is not
1215
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1217
def extract(self, sub_path, file_id=None, format=None):
1218
"""Extract a subtree from this tree.
1220
A new branch will be created, relative to the path for this tree.
1223
segments = osutils.splitpath(path)
1224
transport = self.branch.controldir.root_transport
1225
for name in segments:
1226
transport = transport.clone(name)
1227
transport.ensure_base()
1230
with self.lock_tree_write():
1232
branch_transport = mkdirs(sub_path)
1234
format = self.controldir.cloning_metadir()
1235
branch_transport.ensure_base()
1236
branch_bzrdir = format.initialize_on_transport(branch_transport)
1238
repo = branch_bzrdir.find_repository()
1239
except errors.NoRepositoryPresent:
1240
repo = branch_bzrdir.create_repository()
1241
if not repo.supports_rich_root():
1242
raise errors.RootNotRich()
1243
new_branch = branch_bzrdir.create_branch()
1244
new_branch.pull(self.branch)
1245
for parent_id in self.get_parent_ids():
1246
new_branch.fetch(self.branch, parent_id)
1247
tree_transport = self.controldir.root_transport.clone(sub_path)
1248
if tree_transport.base != branch_transport.base:
1249
tree_bzrdir = format.initialize_on_transport(tree_transport)
1250
tree_bzrdir.set_branch_reference(new_branch)
1252
tree_bzrdir = branch_bzrdir
1253
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1254
wt.set_parent_ids(self.get_parent_ids())
1257
def _get_check_refs(self):
1258
"""Return the references needed to perform a check of this tree.
1260
The default implementation returns no refs, and is only suitable for
1261
trees that have no local caching and can commit on ghosts at any time.
1263
:seealso: breezy.check for details about check_refs.
1267
def copy_content_into(self, tree, revision_id=None):
1268
"""Copy the current content and user files of this tree into tree."""
1269
with self.lock_read():
1270
if revision_id is None:
1271
merge.transform_tree(tree, self)
1273
# TODO now merge from tree.last_revision to revision (to
1274
# preserve user local changes)
1276
other_tree = self.revision_tree(revision_id)
1277
except errors.NoSuchRevision:
1278
other_tree = self.branch.repository.revision_tree(
1281
merge.transform_tree(tree, other_tree)
1282
if revision_id == _mod_revision.NULL_REVISION:
1285
new_parents = [revision_id]
1286
tree.set_parent_ids(new_parents)
1289
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1291
_tree_class = GitWorkingTree
1293
supports_versioned_directories = False
1295
supports_setting_file_ids = False
1297
supports_store_uncommitted = False
1299
supports_leftmost_parent_id_as_ghost = False
1301
supports_righthand_parent_id_as_ghost = False
1303
requires_normalized_unicode_filenames = True
1305
supports_merge_modified = False
1308
def _matchingcontroldir(self):
1309
from .dir import LocalGitControlDirFormat
1310
return LocalGitControlDirFormat()
1312
def get_format_description(self):
1313
return "Git Working Tree"
1315
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1316
accelerator_tree=None, hardlink=False):
1317
"""See WorkingTreeFormat.initialize()."""
1318
if not isinstance(a_controldir, LocalGitDir):
1319
raise errors.IncompatibleFormat(self, a_controldir)
1320
branch = a_controldir.open_branch(nascent_ok=True)
1321
if revision_id is not None:
1322
branch.set_last_revision(revision_id)
1323
wt = GitWorkingTree(
1324
a_controldir, a_controldir.open_repository(), branch)
1325
for hook in MutableTree.hooks['post_build_tree']: