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 self.mapping.is_special_file(name):
541
if self.controldir.is_control_filename(
542
name.decode(osutils._fs_enc, 'replace')):
544
yp = os.path.join(dir_relpath, name)
546
yield yp.decode(osutils._fs_enc)
547
except UnicodeDecodeError:
548
raise errors.BadFilenameEncoding(
552
"""Yield all unversioned files in this WorkingTree.
554
with self.lock_read():
556
[p.decode('utf-8') for p, i in self._recurse_index_entries()])
557
all_paths = set(self._iter_files_recursive(include_dirs=False))
558
return iter(all_paths - index_paths)
560
def _gather_kinds(self, files, kinds):
561
"""See MutableTree._gather_kinds."""
562
with self.lock_tree_write():
563
for pos, f in enumerate(files):
564
if kinds[pos] is None:
565
fullpath = osutils.normpath(self.abspath(f))
567
kind = osutils.file_kind(fullpath)
569
if e.errno == errno.ENOENT:
570
raise errors.NoSuchFile(fullpath)
571
if (kind == 'directory' and f != '' and
572
os.path.exists(os.path.join(fullpath, '.git'))):
573
kind = 'tree-reference'
577
if self._lock_mode != 'w':
578
raise errors.NotWriteLocked(self)
579
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
580
# already in use and GitFile doesn't allow overriding the lock file
582
f = open(self.control_transport.local_abspath('index'), 'wb')
583
# Note that _flush will close the file
589
write_index_dict(shaf, self.index)
591
except BaseException:
594
self._index_dirty = False
596
def has_or_had_id(self, file_id):
597
if self.has_id(file_id):
599
if self.had_id(file_id):
603
def had_id(self, file_id):
604
path = self._basis_fileid_map.lookup_path(file_id)
606
head = self.repository._git.head()
608
# Assume no if basis is not accessible
611
root_tree = self.store[head].tree
615
tree_lookup_path(self.store.__getitem__,
616
root_tree, path.encode('utf-8'))
622
def get_file_mtime(self, path):
623
"""See Tree.get_file_mtime."""
625
return self._lstat(path).st_mtime
627
if e.errno == errno.ENOENT:
628
raise errors.NoSuchFile(path)
631
def is_ignored(self, filename):
632
r"""Check whether the filename matches an ignore pattern.
634
If the file is ignored, returns the pattern which caused it to
635
be ignored, otherwise None. So this can simply be used as a
636
boolean if desired."""
637
if getattr(self, '_global_ignoreglobster', None) is None:
639
ignore_globs.update(ignores.get_runtime_ignores())
640
ignore_globs.update(ignores.get_user_ignores())
641
self._global_ignoreglobster = globbing.ExceptionGlobster(
643
match = self._global_ignoreglobster.match(filename)
644
if match is not None:
647
if self.kind(filename) == 'directory':
649
except errors.NoSuchFile:
651
filename = filename.lstrip('/')
652
ignore_manager = self._get_ignore_manager()
653
ps = list(ignore_manager.find_matching(filename))
656
if not ps[-1].is_exclude:
660
def _get_ignore_manager(self):
661
ignoremanager = getattr(self, '_ignoremanager', None)
662
if ignoremanager is not None:
665
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
666
self._ignoremanager = ignore_manager
667
return ignore_manager
669
def _flush_ignore_list_cache(self):
670
self._ignoremanager = None
672
def set_last_revision(self, revid):
673
if _mod_revision.is_null(revid):
674
self.branch.set_last_revision_info(0, revid)
676
_mod_revision.check_not_reserved_id(revid)
678
self.branch.generate_revision_history(revid)
679
except errors.NoSuchRevision:
680
raise errors.GhostRevisionUnusableHere(revid)
682
def _reset_data(self):
684
head = self.repository._git.head()
686
self._basis_fileid_map = GitFileIdMap({}, self.mapping)
688
self._basis_fileid_map = self.mapping.get_fileid_map(
689
self.store.__getitem__, self.store[head].tree)
690
self._fileid_map = self._basis_fileid_map.copy()
692
def get_file_verifier(self, path, stat_value=None):
693
with self.lock_read():
694
(index, subpath) = self._lookup_index(path.encode('utf-8'))
696
return ("GIT", index[subpath].sha)
698
if self._has_dir(path):
700
raise errors.NoSuchFile(path)
702
def get_file_sha1(self, path, stat_value=None):
703
with self.lock_read():
704
if not self.is_versioned(path):
705
raise errors.NoSuchFile(path)
706
abspath = self.abspath(path)
708
return osutils.sha_file_by_name(abspath)
710
if e.errno in (errno.EISDIR, errno.ENOENT):
714
def revision_tree(self, revid):
715
return self.repository.revision_tree(revid)
717
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
718
mode = stat_result.st_mode
719
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
721
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
722
return self.basis_tree().is_executable(path)
724
def stored_kind(self, path):
725
with self.lock_read():
726
encoded_path = path.encode('utf-8')
727
(index, subpath) = self._lookup_index(encoded_path)
729
return mode_kind(index[subpath].mode)
731
# Maybe it's a directory?
732
if self._has_dir(encoded_path):
734
raise errors.NoSuchFile(path)
736
def _lstat(self, path):
737
return os.lstat(self.abspath(path))
739
def _live_entry(self, path):
740
encoded_path = self.abspath(path.decode('utf-8')).encode(
742
return index_entry_from_path(encoded_path)
744
def is_executable(self, path):
745
with self.lock_read():
746
if getattr(self, "_supports_executable",
747
osutils.supports_executable)():
748
mode = self._lstat(path).st_mode
750
(index, subpath) = self._lookup_index(path.encode('utf-8'))
752
mode = index[subpath].mode
755
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
757
def _is_executable_from_path_and_stat(self, path, stat_result):
758
if getattr(self, "_supports_executable",
759
osutils.supports_executable)():
760
return self._is_executable_from_path_and_stat_from_stat(
763
return self._is_executable_from_path_and_stat_from_basis(
766
def list_files(self, include_root=False, from_dir=None, recursive=True):
767
if from_dir is None or from_dir == '.':
770
fk_entries = {'directory': tree.TreeDirectory,
771
'file': tree.TreeFile,
772
'symlink': tree.TreeLink,
773
'tree-reference': tree.TreeReference}
774
with self.lock_read():
775
root_ie = self._get_dir_ie(u"", None)
776
if include_root and not from_dir:
777
yield "", "V", root_ie.kind, root_ie
778
dir_ids[u""] = root_ie.file_id
780
path_iterator = sorted(
781
self._iter_files_recursive(from_dir, include_dirs=True))
783
encoded_from_dir = self.abspath(from_dir).encode(
785
path_iterator = sorted(
786
[os.path.join(from_dir, name.decode(osutils._fs_enc))
787
for name in os.listdir(encoded_from_dir)
788
if not self.controldir.is_control_filename(
789
name.decode(osutils._fs_enc)) and
790
not self.mapping.is_special_file(
791
name.decode(osutils._fs_enc))])
792
for path in path_iterator:
794
encoded_path = path.encode("utf-8")
795
except UnicodeEncodeError:
796
raise errors.BadFilenameEncoding(
797
path, osutils._fs_enc)
798
(index, index_path) = self._lookup_index(encoded_path)
800
value = index[index_path]
803
kind = self.kind(path)
804
parent, name = posixpath.split(path)
805
for dir_path, dir_ie in self._add_missing_parent_ids(
808
if kind in ('directory', 'tree-reference'):
810
if self._has_dir(encoded_path):
811
ie = self._get_dir_ie(path, self.path2id(path))
813
elif self.is_ignored(path):
815
ie = fk_entries[kind]()
818
ie = fk_entries[kind]()
819
yield (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, ie)
826
ie = fk_entries[kind]()
827
yield (posixpath.relpath(path, from_dir),
828
("I" if self.is_ignored(path) else "?"), kind, ie)
830
def all_file_ids(self):
831
raise errors.UnsupportedOperation(self.all_file_ids, self)
833
def all_versioned_paths(self):
834
with self.lock_read():
836
for path in self.index:
837
if self.mapping.is_special_file(path):
839
path = path.decode("utf-8")
842
path = posixpath.dirname(path).strip("/")
848
def iter_child_entries(self, path):
849
encoded_path = path.encode('utf-8')
850
with self.lock_read():
851
parent_id = self.path2id(path)
853
for item_path, value in self.index.iteritems():
854
decoded_item_path = item_path.decode('utf-8')
855
if self.mapping.is_special_file(item_path):
857
if not osutils.is_inside(path, decoded_item_path):
860
subpath = posixpath.relpath(decoded_item_path, path)
862
dirname = subpath.split('/', 1)[0]
863
file_ie = self._get_dir_ie(
864
posixpath.join(path, dirname), parent_id)
866
(unused_parent, name) = posixpath.split(decoded_item_path)
867
file_ie = self._get_file_ie(
868
name, decoded_item_path, value, parent_id)
870
if not found_any and path != u'':
871
raise errors.NoSuchFile(path)
874
with self.lock_read():
875
conflicts = _mod_conflicts.ConflictList()
876
for item_path, value in self.index.iteritems():
877
if value.flags & FLAG_STAGEMASK:
878
conflicts.append(_mod_conflicts.TextConflict(
879
item_path.decode('utf-8')))
882
def set_conflicts(self, conflicts):
884
for conflict in conflicts:
885
if conflict.typestring in ('text conflict', 'contents conflict'):
886
by_path.add(conflict.path.encode('utf-8'))
888
raise errors.UnsupportedOperation(self.set_conflicts, self)
889
with self.lock_tree_write():
890
for path in self.index:
891
self._set_conflicted(path, path in by_path)
893
def _set_conflicted(self, path, conflicted):
894
trace.mutter('change conflict: %r -> %r', path, conflicted)
895
value = self.index[path]
896
self._index_dirty = True
898
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
900
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
902
def add_conflicts(self, new_conflicts):
903
with self.lock_tree_write():
904
for conflict in new_conflicts:
905
if conflict.typestring in ('text conflict',
906
'contents conflict'):
908
self._set_conflicted(
909
conflict.path.encode('utf-8'), True)
911
raise errors.UnsupportedOperation(
912
self.add_conflicts, self)
914
raise errors.UnsupportedOperation(self.add_conflicts, self)
916
def walkdirs(self, prefix=""):
917
"""Walk the directories of this tree.
919
returns a generator which yields items in the form:
920
((curren_directory_path, fileid),
921
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
924
This API returns a generator, which is only valid during the current
925
tree transaction - within a single lock_read or lock_write duration.
927
If the tree is not locked, it may cause an error to be raised,
928
depending on the tree implementation.
930
from bisect import bisect_left
932
disk_top = self.abspath(prefix)
933
if disk_top.endswith('/'):
934
disk_top = disk_top[:-1]
935
top_strip_len = len(disk_top) + 1
936
inventory_iterator = self._walkdirs(prefix)
937
disk_iterator = osutils.walkdirs(disk_top, prefix)
939
current_disk = next(disk_iterator)
940
disk_finished = False
942
if not (e.errno == errno.ENOENT
943
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
948
current_inv = next(inventory_iterator)
950
except StopIteration:
953
while not inv_finished or not disk_finished:
955
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
956
cur_disk_dir_content) = current_disk
958
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
959
cur_disk_dir_content) = ((None, None), None)
960
if not disk_finished:
961
# strip out .bzr dirs
962
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
963
and len(cur_disk_dir_content) > 0):
964
# osutils.walkdirs can be made nicer -
965
# yield the path-from-prefix rather than the pathjoined
967
bzrdir_loc = bisect_left(cur_disk_dir_content,
969
if (bzrdir_loc < len(cur_disk_dir_content) and
970
self.controldir.is_control_filename(
971
cur_disk_dir_content[bzrdir_loc][0])):
972
# we dont yield the contents of, or, .bzr itself.
973
del cur_disk_dir_content[bzrdir_loc]
975
# everything is unknown
978
# everything is missing
981
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
982
- (current_inv[0][0] < cur_disk_dir_relpath))
984
# disk is before inventory - unknown
985
dirblock = [(relpath, basename, kind, stat, None, None) for
986
relpath, basename, kind, stat, top_path in
987
cur_disk_dir_content]
988
yield (cur_disk_dir_relpath, None), dirblock
990
current_disk = next(disk_iterator)
991
except StopIteration:
994
# inventory is before disk - missing.
995
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
996
for relpath, basename, dkind, stat, fileid, kind in
998
yield (current_inv[0][0], current_inv[0][1]), dirblock
1000
current_inv = next(inventory_iterator)
1001
except StopIteration:
1004
# versioned present directory
1005
# merge the inventory and disk data together
1007
for relpath, subiterator in itertools.groupby(sorted(
1008
current_inv[1] + cur_disk_dir_content,
1009
key=operator.itemgetter(0)), operator.itemgetter(1)):
1010
path_elements = list(subiterator)
1011
if len(path_elements) == 2:
1012
inv_row, disk_row = path_elements
1013
# versioned, present file
1014
dirblock.append((inv_row[0],
1015
inv_row[1], disk_row[2],
1016
disk_row[3], inv_row[4],
1018
elif len(path_elements[0]) == 5:
1021
(path_elements[0][0], path_elements[0][1],
1022
path_elements[0][2], path_elements[0][3],
1024
elif len(path_elements[0]) == 6:
1025
# versioned, absent file.
1027
(path_elements[0][0], path_elements[0][1],
1028
'unknown', None, path_elements[0][4],
1029
path_elements[0][5]))
1031
raise NotImplementedError('unreachable code')
1032
yield current_inv[0], dirblock
1034
current_inv = next(inventory_iterator)
1035
except StopIteration:
1038
current_disk = next(disk_iterator)
1039
except StopIteration:
1040
disk_finished = True
1042
def _walkdirs(self, prefix=u""):
1045
prefix = prefix.encode('utf-8')
1046
per_dir = defaultdict(set)
1048
per_dir[(u'', self.get_root_id())] = set()
1050
def add_entry(path, kind):
1051
if path == b'' or not path.startswith(prefix):
1053
(dirname, child_name) = posixpath.split(path)
1054
add_entry(dirname, 'directory')
1055
dirname = dirname.decode("utf-8")
1056
dir_file_id = self.path2id(dirname)
1057
if not isinstance(value, tuple) or len(value) != 10:
1058
raise ValueError(value)
1059
per_dir[(dirname, dir_file_id)].add(
1060
(path.decode("utf-8"), child_name.decode("utf-8"),
1062
self.path2id(path.decode("utf-8")),
1064
with self.lock_read():
1065
for path, value in self.index.iteritems():
1066
if self.mapping.is_special_file(path):
1068
if not path.startswith(prefix):
1070
add_entry(path, mode_kind(value.mode))
1071
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1073
def get_shelf_manager(self):
1074
raise workingtree.ShelvingUnsupported()
1076
def store_uncommitted(self):
1077
raise errors.StoringUncommittedNotSupported(self)
1079
def apply_inventory_delta(self, changes):
1080
for (old_path, new_path, file_id, ie) in changes:
1081
if old_path is not None:
1082
(index, old_subpath) = self._lookup_index(
1083
old_path.encode('utf-8'))
1085
self._index_del_entry(index, old_subpath)
1089
self._versioned_dirs = None
1090
if new_path is not None and ie.kind != 'directory':
1091
if ie.kind == 'tree-reference':
1092
self._index_add_entry(
1094
reference_revision=ie.reference_revision)
1096
self._index_add_entry(new_path, ie.kind)
1099
def annotate_iter(self, path,
1100
default_revision=_mod_revision.CURRENT_REVISION):
1101
"""See Tree.annotate_iter
1103
This implementation will use the basis tree implementation if possible.
1104
Lines not in the basis are attributed to CURRENT_REVISION
1106
If there are pending merges, lines added by those merges will be
1107
incorrectly attributed to CURRENT_REVISION (but after committing, the
1108
attribution will be correct).
1110
with self.lock_read():
1111
maybe_file_parent_keys = []
1112
for parent_id in self.get_parent_ids():
1114
parent_tree = self.revision_tree(parent_id)
1115
except errors.NoSuchRevisionInTree:
1116
parent_tree = self.branch.repository.revision_tree(
1118
with parent_tree.lock_read():
1119
# TODO(jelmer): Use rename/copy tracker to find path name
1123
kind = parent_tree.kind(parent_path)
1124
except errors.NoSuchFile:
1127
# Note: this is slightly unnecessary, because symlinks
1128
# and directories have a "text" which is the empty
1129
# text, and we know that won't mess up annotations. But
1134
parent_tree.get_file_revision(parent_path))
1135
if parent_text_key not in maybe_file_parent_keys:
1136
maybe_file_parent_keys.append(parent_text_key)
1137
# Now we have the parents of this content
1138
from breezy.annotate import Annotator
1139
from .annotate import AnnotateProvider
1140
annotate_provider = AnnotateProvider(
1141
self.branch.repository._file_change_scanner)
1142
annotator = Annotator(annotate_provider)
1144
from breezy.graph import Graph
1145
graph = Graph(annotate_provider)
1146
heads = graph.heads(maybe_file_parent_keys)
1147
file_parent_keys = []
1148
for key in maybe_file_parent_keys:
1150
file_parent_keys.append(key)
1152
text = self.get_file_text(path)
1153
this_key = (path, default_revision)
1154
annotator.add_special_text(this_key, file_parent_keys, text)
1155
annotations = [(key[-1], line)
1156
for key, line in annotator.annotate_flat(this_key)]
1159
def _rename_one(self, from_rel, to_rel):
1160
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1162
def _build_checkout_with_index(self):
1163
build_index_from_tree(
1164
self.user_transport.local_abspath('.'),
1165
self.control_transport.local_abspath("index"),
1168
if self.branch.head is None
1169
else self.store[self.branch.head].tree)
1171
def reset_state(self, revision_ids=None):
1172
"""Reset the state of the working tree.
1174
This does a hard-reset to a last-known-good state. This is a way to
1175
fix if something got corrupted (like the .git/index file)
1177
with self.lock_tree_write():
1178
if revision_ids is not None:
1179
self.set_parent_ids(revision_ids)
1181
self._index_dirty = True
1182
if self.branch.head is not None:
1183
for entry in self.store.iter_tree_contents(
1184
self.store[self.branch.head].tree):
1185
if not validate_path(entry.path):
1188
if S_ISGITLINK(entry.mode):
1189
pass # TODO(jelmer): record and return submodule paths
1191
# Let's at least try to use the working tree file:
1193
st = self._lstat(self.abspath(
1194
entry.path.decode('utf-8')))
1196
# But if it doesn't exist, we'll make something up.
1197
obj = self.store[entry.sha]
1198
st = os.stat_result((entry.mode, 0, 0, 0,
1200
obj.as_raw_string()), 0,
1202
(index, subpath) = self._lookup_index(entry.path)
1203
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1205
def pull(self, source, overwrite=False, stop_revision=None,
1206
change_reporter=None, possible_transports=None, local=False,
1208
with self.lock_write(), source.lock_read():
1209
old_revision = self.branch.last_revision()
1210
basis_tree = self.basis_tree()
1211
count = self.branch.pull(source, overwrite, stop_revision,
1212
possible_transports=possible_transports,
1214
new_revision = self.branch.last_revision()
1215
if new_revision != old_revision:
1216
with basis_tree.lock_read():
1217
new_basis_tree = self.branch.basis_tree()
1223
change_reporter=change_reporter,
1224
show_base=show_base)
1227
def add_reference(self, sub_tree):
1228
"""Add a TreeReference to the tree, pointing at sub_tree.
1230
:param sub_tree: subtree to add.
1232
with self.lock_tree_write():
1234
sub_tree_path = self.relpath(sub_tree.basedir)
1235
except errors.PathNotChild:
1236
raise BadReferenceTarget(
1237
self, sub_tree, 'Target not inside tree.')
1239
self._add([sub_tree_path], [None], ['tree-reference'])
1241
def _read_submodule_head(self, path):
1242
return read_submodule_head(self.abspath(path))
1244
def get_reference_revision(self, path):
1245
hexsha = self._read_submodule_head(path)
1247
return _mod_revision.NULL_REVISION
1248
return self.branch.lookup_foreign_revision_id(hexsha)
1250
def get_nested_tree(self, path):
1251
return workingtree.WorkingTree.open(self.abspath(path))
1253
def _directory_is_tree_reference(self, relpath):
1254
# as a special case, if a directory contains control files then
1255
# it's a tree reference, except that the root of the tree is not
1256
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1258
def extract(self, sub_path, format=None):
1259
"""Extract a subtree from this tree.
1261
A new branch will be created, relative to the path for this tree.
1264
segments = osutils.splitpath(path)
1265
transport = self.branch.controldir.root_transport
1266
for name in segments:
1267
transport = transport.clone(name)
1268
transport.ensure_base()
1271
with self.lock_tree_write():
1273
branch_transport = mkdirs(sub_path)
1275
format = self.controldir.cloning_metadir()
1276
branch_transport.ensure_base()
1277
branch_bzrdir = format.initialize_on_transport(branch_transport)
1279
repo = branch_bzrdir.find_repository()
1280
except errors.NoRepositoryPresent:
1281
repo = branch_bzrdir.create_repository()
1282
if not repo.supports_rich_root():
1283
raise errors.RootNotRich()
1284
new_branch = branch_bzrdir.create_branch()
1285
new_branch.pull(self.branch)
1286
for parent_id in self.get_parent_ids():
1287
new_branch.fetch(self.branch, parent_id)
1288
tree_transport = self.controldir.root_transport.clone(sub_path)
1289
if tree_transport.base != branch_transport.base:
1290
tree_bzrdir = format.initialize_on_transport(tree_transport)
1291
tree_bzrdir.set_branch_reference(new_branch)
1293
tree_bzrdir = branch_bzrdir
1294
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1295
wt.set_parent_ids(self.get_parent_ids())
1298
def _get_check_refs(self):
1299
"""Return the references needed to perform a check of this tree.
1301
The default implementation returns no refs, and is only suitable for
1302
trees that have no local caching and can commit on ghosts at any time.
1304
:seealso: breezy.check for details about check_refs.
1308
def copy_content_into(self, tree, revision_id=None):
1309
"""Copy the current content and user files of this tree into tree."""
1310
with self.lock_read():
1311
if revision_id is None:
1312
merge.transform_tree(tree, self)
1314
# TODO now merge from tree.last_revision to revision (to
1315
# preserve user local changes)
1317
other_tree = self.revision_tree(revision_id)
1318
except errors.NoSuchRevision:
1319
other_tree = self.branch.repository.revision_tree(
1322
merge.transform_tree(tree, other_tree)
1323
if revision_id == _mod_revision.NULL_REVISION:
1326
new_parents = [revision_id]
1327
tree.set_parent_ids(new_parents)
1330
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1332
_tree_class = GitWorkingTree
1334
supports_versioned_directories = False
1336
supports_setting_file_ids = False
1338
supports_store_uncommitted = False
1340
supports_leftmost_parent_id_as_ghost = False
1342
supports_righthand_parent_id_as_ghost = False
1344
requires_normalized_unicode_filenames = True
1346
supports_merge_modified = False
1348
ignore_filename = ".gitignore"
1351
def _matchingcontroldir(self):
1352
from .dir import LocalGitControlDirFormat
1353
return LocalGitControlDirFormat()
1355
def get_format_description(self):
1356
return "Git Working Tree"
1358
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1359
accelerator_tree=None, hardlink=False):
1360
"""See WorkingTreeFormat.initialize()."""
1361
if not isinstance(a_controldir, LocalGitDir):
1362
raise errors.IncompatibleFormat(self, a_controldir)
1363
branch = a_controldir.open_branch(nascent_ok=True)
1364
if revision_id is not None:
1365
branch.set_last_revision(revision_id)
1366
wt = GitWorkingTree(
1367
a_controldir, a_controldir.open_repository(), branch)
1368
for hook in MutableTree.hooks['post_build_tree']: