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 get_file_mtime(self, path):
598
"""See Tree.get_file_mtime."""
600
return self._lstat(path).st_mtime
602
if e.errno == errno.ENOENT:
603
raise errors.NoSuchFile(path)
606
def is_ignored(self, filename):
607
r"""Check whether the filename matches an ignore pattern.
609
If the file is ignored, returns the pattern which caused it to
610
be ignored, otherwise None. So this can simply be used as a
611
boolean if desired."""
612
if getattr(self, '_global_ignoreglobster', None) is None:
614
ignore_globs.update(ignores.get_runtime_ignores())
615
ignore_globs.update(ignores.get_user_ignores())
616
self._global_ignoreglobster = globbing.ExceptionGlobster(
618
match = self._global_ignoreglobster.match(filename)
619
if match is not None:
622
if self.kind(filename) == 'directory':
624
except errors.NoSuchFile:
626
filename = filename.lstrip('/')
627
ignore_manager = self._get_ignore_manager()
628
ps = list(ignore_manager.find_matching(filename))
631
if not ps[-1].is_exclude:
635
def _get_ignore_manager(self):
636
ignoremanager = getattr(self, '_ignoremanager', None)
637
if ignoremanager is not None:
640
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
641
self._ignoremanager = ignore_manager
642
return ignore_manager
644
def _flush_ignore_list_cache(self):
645
self._ignoremanager = None
647
def set_last_revision(self, revid):
648
if _mod_revision.is_null(revid):
649
self.branch.set_last_revision_info(0, revid)
651
_mod_revision.check_not_reserved_id(revid)
653
self.branch.generate_revision_history(revid)
654
except errors.NoSuchRevision:
655
raise errors.GhostRevisionUnusableHere(revid)
657
def _reset_data(self):
660
def get_file_verifier(self, path, stat_value=None):
661
with self.lock_read():
662
(index, subpath) = self._lookup_index(path.encode('utf-8'))
664
return ("GIT", index[subpath].sha)
666
if self._has_dir(path):
668
raise errors.NoSuchFile(path)
670
def get_file_sha1(self, path, stat_value=None):
671
with self.lock_read():
672
if not self.is_versioned(path):
673
raise errors.NoSuchFile(path)
674
abspath = self.abspath(path)
676
return osutils.sha_file_by_name(abspath)
678
if e.errno in (errno.EISDIR, errno.ENOENT):
682
def revision_tree(self, revid):
683
return self.repository.revision_tree(revid)
685
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
686
mode = stat_result.st_mode
687
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
689
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
690
return self.basis_tree().is_executable(path)
692
def stored_kind(self, path):
693
with self.lock_read():
694
encoded_path = path.encode('utf-8')
695
(index, subpath) = self._lookup_index(encoded_path)
697
return mode_kind(index[subpath].mode)
699
# Maybe it's a directory?
700
if self._has_dir(encoded_path):
702
raise errors.NoSuchFile(path)
704
def _lstat(self, path):
705
return os.lstat(self.abspath(path))
707
def _live_entry(self, path):
708
encoded_path = self.abspath(path.decode('utf-8')).encode(
710
return index_entry_from_path(encoded_path)
712
def is_executable(self, path):
713
with self.lock_read():
714
if self._supports_executable():
715
mode = self._lstat(path).st_mode
717
(index, subpath) = self._lookup_index(path.encode('utf-8'))
719
mode = index[subpath].mode
722
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
724
def _is_executable_from_path_and_stat(self, path, stat_result):
725
if self._supports_executable():
726
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
728
return self._is_executable_from_path_and_stat_from_basis(
731
def list_files(self, include_root=False, from_dir=None, recursive=True):
732
if from_dir is None or from_dir == '.':
735
fk_entries = {'directory': tree.TreeDirectory,
736
'file': tree.TreeFile,
737
'symlink': tree.TreeLink,
738
'tree-reference': tree.TreeReference}
739
with self.lock_read():
740
root_ie = self._get_dir_ie(u"", None)
741
if include_root and not from_dir:
742
yield "", "V", root_ie.kind, root_ie
743
dir_ids[u""] = root_ie.file_id
745
path_iterator = sorted(
746
self._iter_files_recursive(from_dir, include_dirs=True))
748
encoded_from_dir = self.abspath(from_dir).encode(
750
path_iterator = sorted(
751
[os.path.join(from_dir, name.decode(osutils._fs_enc))
752
for name in os.listdir(encoded_from_dir)
753
if not self.controldir.is_control_filename(
754
name.decode(osutils._fs_enc)) and
755
not self.mapping.is_special_file(
756
name.decode(osutils._fs_enc))])
757
for path in path_iterator:
759
encoded_path = path.encode("utf-8")
760
except UnicodeEncodeError:
761
raise errors.BadFilenameEncoding(
762
path, osutils._fs_enc)
763
(index, index_path) = self._lookup_index(encoded_path)
765
value = index[index_path]
768
kind = self.kind(path)
769
parent, name = posixpath.split(path)
770
for dir_path, dir_ie in self._add_missing_parent_ids(
773
if kind in ('directory', 'tree-reference'):
775
if self._has_dir(encoded_path):
776
ie = self._get_dir_ie(path, self.path2id(path))
778
elif self.is_ignored(path):
780
ie = fk_entries[kind]()
783
ie = fk_entries[kind]()
784
yield (posixpath.relpath(path, from_dir), status, kind,
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)
791
ie = fk_entries[kind]()
792
yield (posixpath.relpath(path, from_dir),
793
("I" if self.is_ignored(path) else "?"), kind, ie)
795
def all_file_ids(self):
796
raise errors.UnsupportedOperation(self.all_file_ids, self)
798
def all_versioned_paths(self):
799
with self.lock_read():
801
for path in self.index:
802
if self.mapping.is_special_file(path):
804
path = path.decode("utf-8")
807
path = posixpath.dirname(path).strip("/")
813
def iter_child_entries(self, path):
814
encoded_path = path.encode('utf-8')
815
with self.lock_read():
816
parent_id = self.path2id(path)
818
for item_path, value in self.index.iteritems():
819
decoded_item_path = item_path.decode('utf-8')
820
if self.mapping.is_special_file(item_path):
822
if not osutils.is_inside(path, decoded_item_path):
825
subpath = posixpath.relpath(decoded_item_path, path)
827
dirname = subpath.split('/', 1)[0]
828
file_ie = self._get_dir_ie(
829
posixpath.join(path, dirname), parent_id)
831
(unused_parent, name) = posixpath.split(decoded_item_path)
832
file_ie = self._get_file_ie(
833
name, decoded_item_path, value, parent_id)
835
if not found_any and path != u'':
836
raise errors.NoSuchFile(path)
839
with self.lock_read():
840
conflicts = _mod_conflicts.ConflictList()
841
for item_path, value in self.index.iteritems():
842
if value.flags & FLAG_STAGEMASK:
843
conflicts.append(_mod_conflicts.TextConflict(
844
item_path.decode('utf-8')))
847
def set_conflicts(self, conflicts):
849
for conflict in conflicts:
850
if conflict.typestring in ('text conflict', 'contents conflict'):
851
by_path.add(conflict.path.encode('utf-8'))
853
raise errors.UnsupportedOperation(self.set_conflicts, self)
854
with self.lock_tree_write():
855
for path in self.index:
856
self._set_conflicted(path, path in by_path)
858
def _set_conflicted(self, path, conflicted):
859
trace.mutter('change conflict: %r -> %r', path, conflicted)
860
value = self.index[path]
861
self._index_dirty = True
863
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
865
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
867
def add_conflicts(self, new_conflicts):
868
with self.lock_tree_write():
869
for conflict in new_conflicts:
870
if conflict.typestring in ('text conflict',
871
'contents conflict'):
873
self._set_conflicted(
874
conflict.path.encode('utf-8'), True)
876
raise errors.UnsupportedOperation(
877
self.add_conflicts, self)
879
raise errors.UnsupportedOperation(self.add_conflicts, self)
881
def walkdirs(self, prefix=""):
882
"""Walk the directories of this tree.
884
returns a generator which yields items in the form:
885
((curren_directory_path, fileid),
886
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
889
This API returns a generator, which is only valid during the current
890
tree transaction - within a single lock_read or lock_write duration.
892
If the tree is not locked, it may cause an error to be raised,
893
depending on the tree implementation.
895
from bisect import bisect_left
897
disk_top = self.abspath(prefix)
898
if disk_top.endswith('/'):
899
disk_top = disk_top[:-1]
900
top_strip_len = len(disk_top) + 1
901
inventory_iterator = self._walkdirs(prefix)
902
disk_iterator = osutils.walkdirs(disk_top, prefix)
904
current_disk = next(disk_iterator)
905
disk_finished = False
907
if not (e.errno == errno.ENOENT
908
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
913
current_inv = next(inventory_iterator)
915
except StopIteration:
918
while not inv_finished or not disk_finished:
920
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
921
cur_disk_dir_content) = current_disk
923
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
924
cur_disk_dir_content) = ((None, None), None)
925
if not disk_finished:
926
# strip out .bzr dirs
927
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
928
and len(cur_disk_dir_content) > 0):
929
# osutils.walkdirs can be made nicer -
930
# yield the path-from-prefix rather than the pathjoined
932
bzrdir_loc = bisect_left(cur_disk_dir_content,
934
if (bzrdir_loc < len(cur_disk_dir_content) and
935
self.controldir.is_control_filename(
936
cur_disk_dir_content[bzrdir_loc][0])):
937
# we dont yield the contents of, or, .bzr itself.
938
del cur_disk_dir_content[bzrdir_loc]
940
# everything is unknown
943
# everything is missing
946
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
947
- (current_inv[0][0] < cur_disk_dir_relpath))
949
# disk is before inventory - unknown
950
dirblock = [(relpath, basename, kind, stat, None, None) for
951
relpath, basename, kind, stat, top_path in
952
cur_disk_dir_content]
953
yield (cur_disk_dir_relpath, None), dirblock
955
current_disk = next(disk_iterator)
956
except StopIteration:
959
# inventory is before disk - missing.
960
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
961
for relpath, basename, dkind, stat, fileid, kind in
963
yield (current_inv[0][0], current_inv[0][1]), dirblock
965
current_inv = next(inventory_iterator)
966
except StopIteration:
969
# versioned present directory
970
# merge the inventory and disk data together
972
for relpath, subiterator in itertools.groupby(sorted(
973
current_inv[1] + cur_disk_dir_content,
974
key=operator.itemgetter(0)), operator.itemgetter(1)):
975
path_elements = list(subiterator)
976
if len(path_elements) == 2:
977
inv_row, disk_row = path_elements
978
# versioned, present file
979
dirblock.append((inv_row[0],
980
inv_row[1], disk_row[2],
981
disk_row[3], inv_row[4],
983
elif len(path_elements[0]) == 5:
986
(path_elements[0][0], path_elements[0][1],
987
path_elements[0][2], path_elements[0][3],
989
elif len(path_elements[0]) == 6:
990
# versioned, absent file.
992
(path_elements[0][0], path_elements[0][1],
993
'unknown', None, path_elements[0][4],
994
path_elements[0][5]))
996
raise NotImplementedError('unreachable code')
997
yield current_inv[0], dirblock
999
current_inv = next(inventory_iterator)
1000
except StopIteration:
1003
current_disk = next(disk_iterator)
1004
except StopIteration:
1005
disk_finished = True
1007
def _walkdirs(self, prefix=u""):
1010
prefix = prefix.encode('utf-8')
1011
per_dir = defaultdict(set)
1013
per_dir[(u'', self.path2id(''))] = set()
1015
def add_entry(path, kind):
1016
if path == b'' or not path.startswith(prefix):
1018
(dirname, child_name) = posixpath.split(path)
1019
add_entry(dirname, 'directory')
1020
dirname = dirname.decode("utf-8")
1021
dir_file_id = self.path2id(dirname)
1022
if not isinstance(value, tuple) or len(value) != 10:
1023
raise ValueError(value)
1024
per_dir[(dirname, dir_file_id)].add(
1025
(path.decode("utf-8"), child_name.decode("utf-8"),
1027
self.path2id(path.decode("utf-8")),
1029
with self.lock_read():
1030
for path, value in self.index.iteritems():
1031
if self.mapping.is_special_file(path):
1033
if not path.startswith(prefix):
1035
add_entry(path, mode_kind(value.mode))
1036
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1038
def get_shelf_manager(self):
1039
raise workingtree.ShelvingUnsupported()
1041
def store_uncommitted(self):
1042
raise errors.StoringUncommittedNotSupported(self)
1044
def apply_inventory_delta(self, changes):
1045
for (old_path, new_path, file_id, ie) in changes:
1046
if old_path is not None:
1047
(index, old_subpath) = self._lookup_index(
1048
old_path.encode('utf-8'))
1050
self._index_del_entry(index, old_subpath)
1054
self._versioned_dirs = None
1055
if new_path is not None and ie.kind != 'directory':
1056
if ie.kind == 'tree-reference':
1057
self._index_add_entry(
1059
reference_revision=ie.reference_revision)
1061
self._index_add_entry(new_path, ie.kind)
1064
def annotate_iter(self, path,
1065
default_revision=_mod_revision.CURRENT_REVISION):
1066
"""See Tree.annotate_iter
1068
This implementation will use the basis tree implementation if possible.
1069
Lines not in the basis are attributed to CURRENT_REVISION
1071
If there are pending merges, lines added by those merges will be
1072
incorrectly attributed to CURRENT_REVISION (but after committing, the
1073
attribution will be correct).
1075
with self.lock_read():
1076
maybe_file_parent_keys = []
1077
for parent_id in self.get_parent_ids():
1079
parent_tree = self.revision_tree(parent_id)
1080
except errors.NoSuchRevisionInTree:
1081
parent_tree = self.branch.repository.revision_tree(
1083
with parent_tree.lock_read():
1084
# TODO(jelmer): Use rename/copy tracker to find path name
1088
kind = parent_tree.kind(parent_path)
1089
except errors.NoSuchFile:
1092
# Note: this is slightly unnecessary, because symlinks
1093
# and directories have a "text" which is the empty
1094
# text, and we know that won't mess up annotations. But
1099
parent_tree.get_file_revision(parent_path))
1100
if parent_text_key not in maybe_file_parent_keys:
1101
maybe_file_parent_keys.append(parent_text_key)
1102
# Now we have the parents of this content
1103
from breezy.annotate import Annotator
1104
from .annotate import AnnotateProvider
1105
annotate_provider = AnnotateProvider(
1106
self.branch.repository._file_change_scanner)
1107
annotator = Annotator(annotate_provider)
1109
from breezy.graph import Graph
1110
graph = Graph(annotate_provider)
1111
heads = graph.heads(maybe_file_parent_keys)
1112
file_parent_keys = []
1113
for key in maybe_file_parent_keys:
1115
file_parent_keys.append(key)
1117
text = self.get_file_text(path)
1118
this_key = (path, default_revision)
1119
annotator.add_special_text(this_key, file_parent_keys, text)
1120
annotations = [(key[-1], line)
1121
for key, line in annotator.annotate_flat(this_key)]
1124
def _rename_one(self, from_rel, to_rel):
1125
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1127
def _build_checkout_with_index(self):
1128
build_index_from_tree(
1129
self.user_transport.local_abspath('.'),
1130
self.control_transport.local_abspath("index"),
1133
if self.branch.head is None
1134
else self.store[self.branch.head].tree,
1135
honor_filemode=self._supports_executable())
1137
def reset_state(self, revision_ids=None):
1138
"""Reset the state of the working tree.
1140
This does a hard-reset to a last-known-good state. This is a way to
1141
fix if something got corrupted (like the .git/index file)
1143
with self.lock_tree_write():
1144
if revision_ids is not None:
1145
self.set_parent_ids(revision_ids)
1147
self._index_dirty = True
1148
if self.branch.head is not None:
1149
for entry in self.store.iter_tree_contents(
1150
self.store[self.branch.head].tree):
1151
if not validate_path(entry.path):
1154
if S_ISGITLINK(entry.mode):
1155
pass # TODO(jelmer): record and return submodule paths
1157
# Let's at least try to use the working tree file:
1159
st = self._lstat(self.abspath(
1160
entry.path.decode('utf-8')))
1162
# But if it doesn't exist, we'll make something up.
1163
obj = self.store[entry.sha]
1164
st = os.stat_result((entry.mode, 0, 0, 0,
1166
obj.as_raw_string()), 0,
1168
(index, subpath) = self._lookup_index(entry.path)
1169
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1171
def _update_git_tree(self, old_revision, new_revision, change_reporter=None,
1173
basis_tree = self.revision_tree(old_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)
1185
def pull(self, source, overwrite=False, stop_revision=None,
1186
change_reporter=None, possible_transports=None, local=False,
1188
with self.lock_write(), source.lock_read():
1189
old_revision = self.branch.last_revision()
1190
count = self.branch.pull(source, overwrite, stop_revision,
1191
possible_transports=possible_transports,
1193
self._update_git_tree(
1194
old_revision=old_revision,
1195
new_revision=self.branch.last_revision(),
1196
change_reporter=change_reporter,
1197
show_base=show_base)
1200
def add_reference(self, sub_tree):
1201
"""Add a TreeReference to the tree, pointing at sub_tree.
1203
:param sub_tree: subtree to add.
1205
with self.lock_tree_write():
1207
sub_tree_path = self.relpath(sub_tree.basedir)
1208
except errors.PathNotChild:
1209
raise BadReferenceTarget(
1210
self, sub_tree, 'Target not inside tree.')
1212
self._add([sub_tree_path], [None], ['tree-reference'])
1214
def _read_submodule_head(self, path):
1215
return read_submodule_head(self.abspath(path))
1217
def get_reference_revision(self, path):
1218
hexsha = self._read_submodule_head(path)
1220
return _mod_revision.NULL_REVISION
1221
return self.branch.lookup_foreign_revision_id(hexsha)
1223
def get_nested_tree(self, path):
1224
return workingtree.WorkingTree.open(self.abspath(path))
1226
def _directory_is_tree_reference(self, relpath):
1227
# as a special case, if a directory contains control files then
1228
# it's a tree reference, except that the root of the tree is not
1229
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1231
def extract(self, sub_path, format=None):
1232
"""Extract a subtree from this tree.
1234
A new branch will be created, relative to the path for this tree.
1237
segments = osutils.splitpath(path)
1238
transport = self.branch.controldir.root_transport
1239
for name in segments:
1240
transport = transport.clone(name)
1241
transport.ensure_base()
1244
with self.lock_tree_write():
1246
branch_transport = mkdirs(sub_path)
1248
format = self.controldir.cloning_metadir()
1249
branch_transport.ensure_base()
1250
branch_bzrdir = format.initialize_on_transport(branch_transport)
1252
repo = branch_bzrdir.find_repository()
1253
except errors.NoRepositoryPresent:
1254
repo = branch_bzrdir.create_repository()
1255
if not repo.supports_rich_root():
1256
raise errors.RootNotRich()
1257
new_branch = branch_bzrdir.create_branch()
1258
new_branch.pull(self.branch)
1259
for parent_id in self.get_parent_ids():
1260
new_branch.fetch(self.branch, parent_id)
1261
tree_transport = self.controldir.root_transport.clone(sub_path)
1262
if tree_transport.base != branch_transport.base:
1263
tree_bzrdir = format.initialize_on_transport(tree_transport)
1264
tree_bzrdir.set_branch_reference(new_branch)
1266
tree_bzrdir = branch_bzrdir
1267
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1268
wt.set_parent_ids(self.get_parent_ids())
1271
def _get_check_refs(self):
1272
"""Return the references needed to perform a check of this tree.
1274
The default implementation returns no refs, and is only suitable for
1275
trees that have no local caching and can commit on ghosts at any time.
1277
:seealso: breezy.check for details about check_refs.
1281
def copy_content_into(self, tree, revision_id=None):
1282
"""Copy the current content and user files of this tree into tree."""
1283
with self.lock_read():
1284
if revision_id is None:
1285
merge.transform_tree(tree, self)
1287
# TODO now merge from tree.last_revision to revision (to
1288
# preserve user local changes)
1290
other_tree = self.revision_tree(revision_id)
1291
except errors.NoSuchRevision:
1292
other_tree = self.branch.repository.revision_tree(
1295
merge.transform_tree(tree, other_tree)
1296
if revision_id == _mod_revision.NULL_REVISION:
1299
new_parents = [revision_id]
1300
tree.set_parent_ids(new_parents)
1303
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1305
_tree_class = GitWorkingTree
1307
supports_versioned_directories = False
1309
supports_setting_file_ids = False
1311
supports_store_uncommitted = False
1313
supports_leftmost_parent_id_as_ghost = False
1315
supports_righthand_parent_id_as_ghost = False
1317
requires_normalized_unicode_filenames = True
1319
supports_merge_modified = False
1321
ignore_filename = ".gitignore"
1324
def _matchingcontroldir(self):
1325
from .dir import LocalGitControlDirFormat
1326
return LocalGitControlDirFormat()
1328
def get_format_description(self):
1329
return "Git Working Tree"
1331
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1332
accelerator_tree=None, hardlink=False):
1333
"""See WorkingTreeFormat.initialize()."""
1334
if not isinstance(a_controldir, LocalGitDir):
1335
raise errors.IncompatibleFormat(self, a_controldir)
1336
branch = a_controldir.open_branch(nascent_ok=True)
1337
if revision_id is not None:
1338
branch.set_last_revision(revision_id)
1339
wt = GitWorkingTree(
1340
a_controldir, a_controldir.open_repository(), branch)
1341
for hook in MutableTree.hooks['post_build_tree']: