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"""
21
from collections import defaultdict
23
from dulwich.ignore import (
26
from dulwich.config import ConfigFile as GitConfigFile
27
from dulwich.file import GitFile, FileLocked
28
from dulwich.index import (
31
build_index_from_tree,
32
index_entry_from_path,
33
index_entry_from_stat,
39
from dulwich.object_store import (
42
from dulwich.objects import (
51
branch as _mod_branch,
52
conflicts as _mod_conflicts,
54
controldir as _mod_controldir,
60
revision as _mod_revision,
62
transport as _mod_transport,
67
from ..decorators import (
70
from ..mutabletree import (
82
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
118
def _get_submodule_index(self, relpath):
119
if not isinstance(relpath, bytes):
120
raise TypeError(relpath)
122
info = self._submodule_info()[relpath]
124
index_path = os.path.join(self.basedir, relpath.decode('utf-8'), '.git', 'index')
126
index_path = self.control_transport.local_abspath(
127
posixpath.join('modules', info[1], 'index'))
128
return Index(index_path)
131
"""Lock the repository for read operations.
133
:return: A breezy.lock.LogicalLockResult.
135
if not self._lock_mode:
136
self._lock_mode = 'r'
140
self._lock_count += 1
141
self.branch.lock_read()
142
return lock.LogicalLockResult(self.unlock)
144
def _lock_write_tree(self):
145
if not self._lock_mode:
146
self._lock_mode = 'w'
149
self._index_file = GitFile(
150
self.control_transport.local_abspath('index'), 'wb')
152
raise errors.LockContention('index')
154
elif self._lock_mode == 'r':
155
raise errors.ReadOnlyError(self)
157
self._lock_count += 1
159
def lock_tree_write(self):
160
self.branch.lock_read()
162
self._lock_write_tree()
163
return lock.LogicalLockResult(self.unlock)
164
except BaseException:
168
def lock_write(self, token=None):
169
self.branch.lock_write()
171
self._lock_write_tree()
172
return lock.LogicalLockResult(self.unlock)
173
except BaseException:
178
return self._lock_count >= 1
180
def get_physical_lock_status(self):
183
def break_lock(self):
185
self.control_transport.delete('index.lock')
186
except errors.NoSuchFile:
188
self.branch.break_lock()
190
@only_raises(errors.LockNotHeld, errors.LockBroken)
192
if not self._lock_count:
193
return lock.cant_unlock_not_held(self)
196
self._lock_count -= 1
197
if self._lock_count > 0:
199
if self._index_file is not None:
200
if self._index_dirty:
201
self._flush(self._index_file)
202
self._index_file.close()
204
# Something else already triggered a write of the index
205
# file by calling .flush()
206
self._index_file.abort()
207
self._index_file = None
208
self._lock_mode = None
216
def _detect_case_handling(self):
218
self._transport.stat(".git/cOnFiG")
219
except errors.NoSuchFile:
220
self.case_sensitive = True
222
self.case_sensitive = False
224
def get_transform(self, pb=None):
225
from ..transform import TreeTransform
226
return TreeTransform(self, pb=pb)
228
def merge_modified(self):
231
def set_merge_modified(self, modified_hashes):
232
raise errors.UnsupportedOperation(self.set_merge_modified, self)
234
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
235
self.set_parent_ids([p for p, t in parents_list])
237
def _set_merges_from_parent_ids(self, rhs_parent_ids):
239
merges = [self.branch.lookup_bzr_revision_id(
240
revid)[0] for revid in rhs_parent_ids]
241
except errors.NoSuchRevision as e:
242
raise errors.GhostRevisionUnusableHere(e.revision)
244
self.control_transport.put_bytes(
245
'MERGE_HEAD', b'\n'.join(merges),
246
mode=self.controldir._get_file_mode())
249
self.control_transport.delete('MERGE_HEAD')
250
except errors.NoSuchFile:
253
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
254
"""Set the parent ids to revision_ids.
256
See also set_parent_trees. This api will try to retrieve the tree data
257
for each element of revision_ids from the trees repository. If you have
258
tree data already available, it is more efficient to use
259
set_parent_trees rather than set_parent_ids. set_parent_ids is however
260
an easier API to use.
262
:param revision_ids: The revision_ids to set as the parent ids of this
263
working tree. Any of these may be ghosts.
265
with self.lock_tree_write():
266
self._check_parents_for_ghosts(
267
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
268
for revision_id in revision_ids:
269
_mod_revision.check_not_reserved_id(revision_id)
271
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
273
if len(revision_ids) > 0:
274
self.set_last_revision(revision_ids[0])
276
self.set_last_revision(_mod_revision.NULL_REVISION)
278
self._set_merges_from_parent_ids(revision_ids[1:])
280
def get_parent_ids(self):
281
"""See Tree.get_parent_ids.
283
This implementation reads the pending merges list and last_revision
284
value and uses that to decide what the parents list should be.
286
last_rev = _mod_revision.ensure_null(self._last_revision())
287
if _mod_revision.NULL_REVISION == last_rev:
292
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
293
except errors.NoSuchFile:
296
for l in osutils.split_lines(merges_bytes):
297
revision_id = l.rstrip(b'\n')
299
self.branch.lookup_foreign_revision_id(revision_id))
302
def check_state(self):
303
"""Check that the working state is/isn't valid."""
306
def remove(self, files, verbose=False, to_file=None, keep_files=True,
308
"""Remove nominated files from the working tree metadata.
310
:param files: File paths relative to the basedir.
311
:param keep_files: If true, the files will also be kept.
312
:param force: Delete files and directories, even if they are changed
313
and even if the directories are not empty.
315
if not isinstance(files, list):
321
def backup(file_to_backup):
322
abs_path = self.abspath(file_to_backup)
323
backup_name = self.controldir._available_backup_name(
325
osutils.rename(abs_path, self.abspath(backup_name))
326
return "removed %s (but kept a copy: %s)" % (
327
file_to_backup, backup_name)
329
# Sort needed to first handle directory content before the directory
334
def recurse_directory_to_add_files(directory):
335
# Recurse directory and add all files
336
# so we can check if they have changed.
337
for parent_info, file_infos in self.walkdirs(directory):
338
for relpath, basename, kind, lstat, fileid, kind in file_infos:
339
# Is it versioned or ignored?
340
if self.is_versioned(relpath):
341
# Add nested content for deletion.
342
all_files.add(relpath)
344
# Files which are not versioned
345
# should be treated as unknown.
346
files_to_backup.append(relpath)
348
with self.lock_tree_write():
349
for filepath in files:
350
# Get file name into canonical form.
351
abspath = self.abspath(filepath)
352
filepath = self.relpath(abspath)
355
all_files.add(filepath)
356
recurse_directory_to_add_files(filepath)
358
files = list(all_files)
361
return # nothing to do
363
# Sort needed to first handle directory content before the
365
files.sort(reverse=True)
367
# Bail out if we are going to delete files we shouldn't
368
if not keep_files and not force:
369
for change in self.iter_changes(
370
self.basis_tree(), include_unchanged=True,
371
require_versioned=False, want_unversioned=True,
372
specific_files=files):
373
if change.versioned[0] is False:
374
# The record is unknown or newly added
375
files_to_backup.append(change.path[1])
376
files_to_backup.extend(
377
osutils.parent_directories(change.path[1]))
378
elif (change.changed_content and (change.kind[1] is not None)
379
and osutils.is_inside_any(files, change.path[1])):
380
# Versioned and changed, but not deleted, and still
381
# in one of the dirs to be deleted.
382
files_to_backup.append(change.path[1])
383
files_to_backup.extend(
384
osutils.parent_directories(change.path[1]))
392
except errors.NoSuchFile:
395
abs_path = self.abspath(f)
397
# having removed it, it must be either ignored or unknown
398
if self.is_ignored(f):
402
kind_ch = osutils.kind_marker(kind)
403
to_file.write(new_status + ' ' + f + kind_ch + '\n')
405
message = "%s does not exist" % (f, )
408
if f in files_to_backup and not force:
411
if kind == 'directory':
412
osutils.rmtree(abs_path)
414
osutils.delete_any(abs_path)
415
message = "deleted %s" % (f,)
417
message = "removed %s" % (f,)
418
self._unversion_path(f)
420
# print only one message (if any) per file.
421
if message is not None:
423
self._versioned_dirs = None
425
def smart_add(self, file_list, recurse=True, action=None, save=True):
429
# expand any symlinks in the directory part, while leaving the
431
# only expanding if symlinks are supported avoids windows path bugs
432
if self.supports_symlinks():
433
file_list = list(map(osutils.normalizepath, file_list))
435
conflicts_related = set()
436
for c in self.conflicts():
437
conflicts_related.update(c.associated_filenames())
443
def call_action(filepath, kind):
446
if action is not None:
447
parent_path = posixpath.dirname(filepath)
448
parent_id = self.path2id(parent_path)
449
parent_ie = self._get_dir_ie(parent_path, parent_id)
450
file_id = action(self, parent_ie, filepath, kind)
451
if file_id is not None:
452
raise workingtree.SettingFileIdUnsupported()
454
with self.lock_tree_write():
455
for filepath in osutils.canonical_relpaths(
456
self.basedir, file_list):
457
filepath, can_access = osutils.normalized_filename(filepath)
459
raise errors.InvalidNormalization(filepath)
461
abspath = self.abspath(filepath)
462
kind = osutils.file_kind(abspath)
463
if kind in ("file", "symlink"):
464
(index, subpath) = self._lookup_index(
465
filepath.encode('utf-8'))
469
call_action(filepath, kind)
471
self._index_add_entry(filepath, kind)
472
added.append(filepath)
473
elif kind == "directory":
474
(index, subpath) = self._lookup_index(
475
filepath.encode('utf-8'))
476
if subpath not in index:
477
call_action(filepath, kind)
479
user_dirs.append(filepath)
481
raise errors.BadFileKindError(filename=abspath, kind=kind)
482
for user_dir in user_dirs:
483
abs_user_dir = self.abspath(user_dir)
486
transport = _mod_transport.get_transport_from_path(
488
_mod_controldir.ControlDirFormat.find_format(transport)
490
except errors.NotBranchError:
492
except errors.UnsupportedFormatError:
497
trace.warning('skipping nested tree %r', abs_user_dir)
500
for name in os.listdir(abs_user_dir):
501
subp = os.path.join(user_dir, name)
502
if (self.is_control_filename(subp) or
503
self.mapping.is_special_file(subp)):
505
ignore_glob = self.is_ignored(subp)
506
if ignore_glob is not None:
507
ignored.setdefault(ignore_glob, []).append(subp)
509
abspath = self.abspath(subp)
510
kind = osutils.file_kind(abspath)
511
if kind == "directory":
512
user_dirs.append(subp)
514
(index, subpath) = self._lookup_index(
515
subp.encode('utf-8'))
519
if subp in conflicts_related:
521
call_action(subp, kind)
523
self._index_add_entry(subp, kind)
525
return added, ignored
527
def has_filename(self, filename):
528
return osutils.lexists(self.abspath(filename))
530
def _iter_files_recursive(self, from_dir=None, include_dirs=False,
531
recurse_nested=False):
534
if not isinstance(from_dir, str):
535
raise TypeError(from_dir)
536
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
537
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
538
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
539
if self.controldir.is_control_filename(
540
dir_relpath.decode(osutils._fs_enc)):
542
for name in list(dirnames):
543
if self.controldir.is_control_filename(
544
name.decode(osutils._fs_enc)):
545
dirnames.remove(name)
547
relpath = os.path.join(dir_relpath, name)
548
if not recurse_nested and self._directory_is_tree_reference(relpath.decode(osutils._fs_enc)):
549
dirnames.remove(name)
552
yield relpath.decode(osutils._fs_enc)
553
except UnicodeDecodeError:
554
raise errors.BadFilenameEncoding(
555
relpath, osutils._fs_enc)
556
if not self.is_versioned(relpath.decode(osutils._fs_enc)):
557
dirnames.remove(name)
558
for name in filenames:
559
if self.mapping.is_special_file(name):
561
if self.controldir.is_control_filename(
562
name.decode(osutils._fs_enc, 'replace')):
564
yp = os.path.join(dir_relpath, name)
566
yield yp.decode(osutils._fs_enc)
567
except UnicodeDecodeError:
568
raise errors.BadFilenameEncoding(
572
"""Yield all unversioned files in this WorkingTree.
574
with self.lock_read():
576
[p.decode('utf-8') for p, i in self._recurse_index_entries()])
577
all_paths = set(self._iter_files_recursive(include_dirs=False))
578
return iter(all_paths - index_paths)
580
def _gather_kinds(self, files, kinds):
581
"""See MutableTree._gather_kinds."""
582
with self.lock_tree_write():
583
for pos, f in enumerate(files):
584
if kinds[pos] is None:
585
fullpath = osutils.normpath(self.abspath(f))
587
kind = osutils.file_kind(fullpath)
589
if e.errno == errno.ENOENT:
590
raise errors.NoSuchFile(fullpath)
591
if f != '' and self._directory_is_tree_reference(f):
592
kind = 'tree-reference'
596
if self._lock_mode != 'w':
597
raise errors.NotWriteLocked(self)
598
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
599
# already in use and GitFile doesn't allow overriding the lock file
601
f = open(self.control_transport.local_abspath('index'), 'wb')
602
# Note that _flush will close the file
608
write_index_dict(shaf, self.index)
610
except BaseException:
613
self._index_dirty = False
615
def get_file_mtime(self, path):
616
"""See Tree.get_file_mtime."""
618
return self._lstat(path).st_mtime
620
if e.errno == errno.ENOENT:
621
raise errors.NoSuchFile(path)
624
def is_ignored(self, filename):
625
r"""Check whether the filename matches an ignore pattern.
627
If the file is ignored, returns the pattern which caused it to
628
be ignored, otherwise None. So this can simply be used as a
629
boolean if desired."""
630
if getattr(self, '_global_ignoreglobster', None) is None:
632
ignore_globs.update(ignores.get_runtime_ignores())
633
ignore_globs.update(ignores.get_user_ignores())
634
self._global_ignoreglobster = globbing.ExceptionGlobster(
636
match = self._global_ignoreglobster.match(filename)
637
if match is not None:
640
if self.kind(filename) == 'directory':
642
except errors.NoSuchFile:
644
filename = filename.lstrip('/')
645
ignore_manager = self._get_ignore_manager()
646
ps = list(ignore_manager.find_matching(filename))
649
if not ps[-1].is_exclude:
653
def _get_ignore_manager(self):
654
ignoremanager = getattr(self, '_ignoremanager', None)
655
if ignoremanager is not None:
658
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
659
self._ignoremanager = ignore_manager
660
return ignore_manager
662
def _flush_ignore_list_cache(self):
663
self._ignoremanager = None
665
def set_last_revision(self, revid):
666
if _mod_revision.is_null(revid):
667
self.branch.set_last_revision_info(0, revid)
669
_mod_revision.check_not_reserved_id(revid)
671
self.branch.generate_revision_history(revid)
672
except errors.NoSuchRevision:
673
raise errors.GhostRevisionUnusableHere(revid)
675
def _reset_data(self):
678
def get_file_verifier(self, path, stat_value=None):
679
with self.lock_read():
680
(index, subpath) = self._lookup_index(path.encode('utf-8'))
682
return ("GIT", index[subpath].sha)
684
if self._has_dir(path):
686
raise errors.NoSuchFile(path)
688
def get_file_sha1(self, path, stat_value=None):
689
with self.lock_read():
690
if not self.is_versioned(path):
691
raise errors.NoSuchFile(path)
692
abspath = self.abspath(path)
694
return osutils.sha_file_by_name(abspath)
696
if e.errno in (errno.EISDIR, errno.ENOENT):
700
def revision_tree(self, revid):
701
return self.repository.revision_tree(revid)
703
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
704
mode = stat_result.st_mode
705
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
707
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
708
return self.basis_tree().is_executable(path)
710
def stored_kind(self, path):
711
with self.lock_read():
712
encoded_path = path.encode('utf-8')
713
(index, subpath) = self._lookup_index(encoded_path)
715
return mode_kind(index[subpath].mode)
717
# Maybe it's a directory?
718
if self._has_dir(encoded_path):
720
raise errors.NoSuchFile(path)
722
def _lstat(self, path):
723
return os.lstat(self.abspath(path))
725
def _live_entry(self, path):
726
encoded_path = self.abspath(path.decode('utf-8')).encode(
728
return index_entry_from_path(encoded_path)
730
def is_executable(self, path):
731
with self.lock_read():
732
if self._supports_executable():
733
mode = self._lstat(path).st_mode
735
(index, subpath) = self._lookup_index(path.encode('utf-8'))
737
mode = index[subpath].mode
740
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
742
def _is_executable_from_path_and_stat(self, path, stat_result):
743
if self._supports_executable():
744
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
746
return self._is_executable_from_path_and_stat_from_basis(
749
def list_files(self, include_root=False, from_dir=None, recursive=True,
750
recurse_nested=False):
751
if from_dir is None or from_dir == '.':
754
fk_entries = {'directory': tree.TreeDirectory,
755
'file': tree.TreeFile,
756
'symlink': tree.TreeLink,
757
'tree-reference': tree.TreeReference}
758
with self.lock_read():
759
root_ie = self._get_dir_ie(u"", None)
760
if include_root and not from_dir:
761
yield "", "V", root_ie.kind, root_ie
762
dir_ids[u""] = root_ie.file_id
764
path_iterator = sorted(
765
self._iter_files_recursive(
766
from_dir, include_dirs=True,
767
recurse_nested=recurse_nested))
769
encoded_from_dir = self.abspath(from_dir).encode(
771
path_iterator = sorted(
772
[os.path.join(from_dir, name.decode(osutils._fs_enc))
773
for name in os.listdir(encoded_from_dir)
774
if not self.controldir.is_control_filename(
775
name.decode(osutils._fs_enc)) and
776
not self.mapping.is_special_file(
777
name.decode(osutils._fs_enc))])
778
for path in path_iterator:
780
encoded_path = path.encode("utf-8")
781
except UnicodeEncodeError:
782
raise errors.BadFilenameEncoding(
783
path, osutils._fs_enc)
784
(index, index_path) = self._lookup_index(encoded_path)
786
value = index[index_path]
789
kind = self.kind(path)
790
parent, name = posixpath.split(path)
791
for dir_path, dir_ie in self._add_missing_parent_ids(
794
if kind == 'tree-reference' and recurse_nested:
795
ie = self._get_dir_ie(path, self.path2id(path))
796
yield (posixpath.relpath(path, from_dir), 'V', 'directory',
799
if kind == 'directory':
801
if self._has_dir(encoded_path):
802
ie = self._get_dir_ie(path, self.path2id(path))
804
elif self.is_ignored(path):
806
ie = fk_entries[kind]()
809
ie = fk_entries[kind]()
810
yield (posixpath.relpath(path, from_dir), status, kind,
813
if value is not None:
814
ie = self._get_file_ie(name, path, value, dir_ids[parent])
815
yield (posixpath.relpath(path, from_dir), "V", ie.kind, ie)
817
ie = fk_entries[kind]()
818
yield (posixpath.relpath(path, from_dir),
819
("I" if self.is_ignored(path) else "?"), kind, ie)
821
def all_file_ids(self):
822
raise errors.UnsupportedOperation(self.all_file_ids, self)
824
def all_versioned_paths(self):
825
with self.lock_read():
827
for path in self.index:
828
if self.mapping.is_special_file(path):
830
path = path.decode("utf-8")
833
path = posixpath.dirname(path).strip("/")
839
def iter_child_entries(self, path):
840
encoded_path = path.encode('utf-8')
841
with self.lock_read():
842
parent_id = self.path2id(path)
844
for item_path, value in self.index.iteritems():
845
decoded_item_path = item_path.decode('utf-8')
846
if self.mapping.is_special_file(item_path):
848
if not osutils.is_inside(path, decoded_item_path):
851
subpath = posixpath.relpath(decoded_item_path, path)
853
dirname = subpath.split('/', 1)[0]
854
file_ie = self._get_dir_ie(
855
posixpath.join(path, dirname), parent_id)
857
(unused_parent, name) = posixpath.split(decoded_item_path)
858
file_ie = self._get_file_ie(
859
name, decoded_item_path, value, parent_id)
861
if not found_any and path != u'':
862
raise errors.NoSuchFile(path)
865
with self.lock_read():
866
conflicts = _mod_conflicts.ConflictList()
867
for item_path, value in self.index.iteritems():
868
if value.flags & FLAG_STAGEMASK:
869
conflicts.append(_mod_conflicts.TextConflict(
870
item_path.decode('utf-8')))
873
def set_conflicts(self, conflicts):
875
for conflict in conflicts:
876
if conflict.typestring in ('text conflict', 'contents conflict'):
877
by_path.add(conflict.path.encode('utf-8'))
879
raise errors.UnsupportedOperation(self.set_conflicts, self)
880
with self.lock_tree_write():
881
for path in self.index:
882
self._set_conflicted(path, path in by_path)
884
def _set_conflicted(self, path, conflicted):
885
trace.mutter('change conflict: %r -> %r', path, conflicted)
886
value = self.index[path]
887
self._index_dirty = True
889
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
891
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
893
def add_conflicts(self, new_conflicts):
894
with self.lock_tree_write():
895
for conflict in new_conflicts:
896
if conflict.typestring in ('text conflict',
897
'contents conflict'):
899
self._set_conflicted(
900
conflict.path.encode('utf-8'), True)
902
raise errors.UnsupportedOperation(
903
self.add_conflicts, self)
905
raise errors.UnsupportedOperation(self.add_conflicts, self)
907
def walkdirs(self, prefix=""):
908
"""Walk the directories of this tree.
910
returns a generator which yields items in the form:
911
((curren_directory_path, fileid),
912
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
915
This API returns a generator, which is only valid during the current
916
tree transaction - within a single lock_read or lock_write duration.
918
If the tree is not locked, it may cause an error to be raised,
919
depending on the tree implementation.
921
from bisect import bisect_left
923
disk_top = self.abspath(prefix)
924
if disk_top.endswith('/'):
925
disk_top = disk_top[:-1]
926
top_strip_len = len(disk_top) + 1
927
inventory_iterator = self._walkdirs(prefix)
928
disk_iterator = osutils.walkdirs(disk_top, prefix)
930
current_disk = next(disk_iterator)
931
disk_finished = False
933
if not (e.errno == errno.ENOENT
934
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
939
current_inv = next(inventory_iterator)
941
except StopIteration:
944
while not inv_finished or not disk_finished:
946
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
947
cur_disk_dir_content) = current_disk
949
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
950
cur_disk_dir_content) = ((None, None), None)
951
if not disk_finished:
952
# strip out .bzr dirs
953
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
954
and len(cur_disk_dir_content) > 0):
955
# osutils.walkdirs can be made nicer -
956
# yield the path-from-prefix rather than the pathjoined
958
bzrdir_loc = bisect_left(cur_disk_dir_content,
960
if (bzrdir_loc < len(cur_disk_dir_content) and
961
self.controldir.is_control_filename(
962
cur_disk_dir_content[bzrdir_loc][0])):
963
# we dont yield the contents of, or, .bzr itself.
964
del cur_disk_dir_content[bzrdir_loc]
966
# everything is unknown
969
# everything is missing
972
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
973
- (current_inv[0][0] < cur_disk_dir_relpath))
975
# disk is before inventory - unknown
976
dirblock = [(relpath, basename, kind, stat, None, None) for
977
relpath, basename, kind, stat, top_path in
978
cur_disk_dir_content]
979
yield (cur_disk_dir_relpath, None), dirblock
981
current_disk = next(disk_iterator)
982
except StopIteration:
985
# inventory is before disk - missing.
986
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
987
for relpath, basename, dkind, stat, fileid, kind in
989
yield (current_inv[0][0], current_inv[0][1]), dirblock
991
current_inv = next(inventory_iterator)
992
except StopIteration:
995
# versioned present directory
996
# merge the inventory and disk data together
998
for relpath, subiterator in itertools.groupby(sorted(
999
current_inv[1] + cur_disk_dir_content,
1000
key=operator.itemgetter(0)), operator.itemgetter(1)):
1001
path_elements = list(subiterator)
1002
if len(path_elements) == 2:
1003
inv_row, disk_row = path_elements
1004
# versioned, present file
1005
dirblock.append((inv_row[0],
1006
inv_row[1], disk_row[2],
1007
disk_row[3], inv_row[4],
1009
elif len(path_elements[0]) == 5:
1012
(path_elements[0][0], path_elements[0][1],
1013
path_elements[0][2], path_elements[0][3],
1015
elif len(path_elements[0]) == 6:
1016
# versioned, absent file.
1018
(path_elements[0][0], path_elements[0][1],
1019
'unknown', None, path_elements[0][4],
1020
path_elements[0][5]))
1022
raise NotImplementedError('unreachable code')
1023
yield current_inv[0], dirblock
1025
current_inv = next(inventory_iterator)
1026
except StopIteration:
1029
current_disk = next(disk_iterator)
1030
except StopIteration:
1031
disk_finished = True
1033
def _walkdirs(self, prefix=u""):
1036
prefix = prefix.encode('utf-8')
1037
per_dir = defaultdict(set)
1039
per_dir[(u'', self.path2id(''))] = set()
1041
def add_entry(path, kind):
1042
if path == b'' or not path.startswith(prefix):
1044
(dirname, child_name) = posixpath.split(path)
1045
add_entry(dirname, 'directory')
1046
dirname = dirname.decode("utf-8")
1047
dir_file_id = self.path2id(dirname)
1048
if not isinstance(value, tuple) or len(value) != 10:
1049
raise ValueError(value)
1050
per_dir[(dirname, dir_file_id)].add(
1051
(path.decode("utf-8"), child_name.decode("utf-8"),
1053
self.path2id(path.decode("utf-8")),
1055
with self.lock_read():
1056
for path, value in self.index.iteritems():
1057
if self.mapping.is_special_file(path):
1059
if not path.startswith(prefix):
1061
add_entry(path, mode_kind(value.mode))
1062
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1064
def get_shelf_manager(self):
1065
raise workingtree.ShelvingUnsupported()
1067
def store_uncommitted(self):
1068
raise errors.StoringUncommittedNotSupported(self)
1070
def apply_inventory_delta(self, changes):
1071
for (old_path, new_path, file_id, ie) in changes:
1072
if old_path is not None:
1073
(index, old_subpath) = self._lookup_index(
1074
old_path.encode('utf-8'))
1076
self._index_del_entry(index, old_subpath)
1080
self._versioned_dirs = None
1081
if new_path is not None and ie.kind != 'directory':
1082
if ie.kind == 'tree-reference':
1083
self._index_add_entry(
1085
reference_revision=ie.reference_revision)
1087
self._index_add_entry(new_path, ie.kind)
1090
def annotate_iter(self, path,
1091
default_revision=_mod_revision.CURRENT_REVISION):
1092
"""See Tree.annotate_iter
1094
This implementation will use the basis tree implementation if possible.
1095
Lines not in the basis are attributed to CURRENT_REVISION
1097
If there are pending merges, lines added by those merges will be
1098
incorrectly attributed to CURRENT_REVISION (but after committing, the
1099
attribution will be correct).
1101
with self.lock_read():
1102
maybe_file_parent_keys = []
1103
for parent_id in self.get_parent_ids():
1105
parent_tree = self.revision_tree(parent_id)
1106
except errors.NoSuchRevisionInTree:
1107
parent_tree = self.branch.repository.revision_tree(
1109
with parent_tree.lock_read():
1110
# TODO(jelmer): Use rename/copy tracker to find path name
1114
kind = parent_tree.kind(parent_path)
1115
except errors.NoSuchFile:
1118
# Note: this is slightly unnecessary, because symlinks
1119
# and directories have a "text" which is the empty
1120
# text, and we know that won't mess up annotations. But
1125
parent_tree.get_file_revision(parent_path))
1126
if parent_text_key not in maybe_file_parent_keys:
1127
maybe_file_parent_keys.append(parent_text_key)
1128
# Now we have the parents of this content
1129
from breezy.annotate import Annotator
1130
from .annotate import AnnotateProvider
1131
annotate_provider = AnnotateProvider(
1132
self.branch.repository._file_change_scanner)
1133
annotator = Annotator(annotate_provider)
1135
from breezy.graph import Graph
1136
graph = Graph(annotate_provider)
1137
heads = graph.heads(maybe_file_parent_keys)
1138
file_parent_keys = []
1139
for key in maybe_file_parent_keys:
1141
file_parent_keys.append(key)
1143
text = self.get_file_text(path)
1144
this_key = (path, default_revision)
1145
annotator.add_special_text(this_key, file_parent_keys, text)
1146
annotations = [(key[-1], line)
1147
for key, line in annotator.annotate_flat(this_key)]
1150
def _rename_one(self, from_rel, to_rel):
1151
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1153
def _build_checkout_with_index(self):
1154
build_index_from_tree(
1155
self.user_transport.local_abspath('.'),
1156
self.control_transport.local_abspath("index"),
1159
if self.branch.head is None
1160
else self.store[self.branch.head].tree,
1161
honor_filemode=self._supports_executable())
1163
def reset_state(self, revision_ids=None):
1164
"""Reset the state of the working tree.
1166
This does a hard-reset to a last-known-good state. This is a way to
1167
fix if something got corrupted (like the .git/index file)
1169
with self.lock_tree_write():
1170
if revision_ids is not None:
1171
self.set_parent_ids(revision_ids)
1173
self._index_dirty = True
1174
if self.branch.head is not None:
1175
for entry in self.store.iter_tree_contents(
1176
self.store[self.branch.head].tree):
1177
if not validate_path(entry.path):
1180
if S_ISGITLINK(entry.mode):
1181
pass # TODO(jelmer): record and return submodule paths
1183
# Let's at least try to use the working tree file:
1185
st = self._lstat(self.abspath(
1186
entry.path.decode('utf-8')))
1188
# But if it doesn't exist, we'll make something up.
1189
obj = self.store[entry.sha]
1190
st = os.stat_result((entry.mode, 0, 0, 0,
1192
obj.as_raw_string()), 0,
1194
(index, subpath) = self._lookup_index(entry.path)
1195
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1197
def _update_git_tree(self, old_revision, new_revision, change_reporter=None,
1199
basis_tree = self.revision_tree(old_revision)
1200
if new_revision != old_revision:
1201
with basis_tree.lock_read():
1202
new_basis_tree = self.branch.basis_tree()
1208
change_reporter=change_reporter,
1209
show_base=show_base)
1211
def pull(self, source, overwrite=False, stop_revision=None,
1212
change_reporter=None, possible_transports=None, local=False,
1214
with self.lock_write(), source.lock_read():
1215
old_revision = self.branch.last_revision()
1216
count = self.branch.pull(source, overwrite, stop_revision,
1217
possible_transports=possible_transports,
1219
self._update_git_tree(
1220
old_revision=old_revision,
1221
new_revision=self.branch.last_revision(),
1222
change_reporter=change_reporter,
1223
show_base=show_base)
1226
def add_reference(self, sub_tree):
1227
"""Add a TreeReference to the tree, pointing at sub_tree.
1229
:param sub_tree: subtree to add.
1231
with self.lock_tree_write():
1233
sub_tree_path = self.relpath(sub_tree.basedir)
1234
except errors.PathNotChild:
1235
raise BadReferenceTarget(
1236
self, sub_tree, 'Target not inside tree.')
1238
self._add([sub_tree_path], [None], ['tree-reference'])
1240
def _read_submodule_head(self, path):
1241
return read_submodule_head(self.abspath(path))
1243
def get_reference_revision(self, path, branch=None):
1244
hexsha = self._read_submodule_head(path)
1246
return _mod_revision.NULL_REVISION
1247
return self.branch.lookup_foreign_revision_id(hexsha)
1249
def get_nested_tree(self, path):
1250
return workingtree.WorkingTree.open(self.abspath(path))
1252
def _directory_is_tree_reference(self, relpath):
1253
# as a special case, if a directory contains control files then
1254
# it's a tree reference, except that the root of the tree is not
1255
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1257
def extract(self, sub_path, format=None):
1258
"""Extract a subtree from this tree.
1260
A new branch will be created, relative to the path for this tree.
1263
segments = osutils.splitpath(path)
1264
transport = self.branch.controldir.root_transport
1265
for name in segments:
1266
transport = transport.clone(name)
1267
transport.ensure_base()
1270
with self.lock_tree_write():
1272
branch_transport = mkdirs(sub_path)
1274
format = self.controldir.cloning_metadir()
1275
branch_transport.ensure_base()
1276
branch_bzrdir = format.initialize_on_transport(branch_transport)
1278
repo = branch_bzrdir.find_repository()
1279
except errors.NoRepositoryPresent:
1280
repo = branch_bzrdir.create_repository()
1281
if not repo.supports_rich_root():
1282
raise errors.RootNotRich()
1283
new_branch = branch_bzrdir.create_branch()
1284
new_branch.pull(self.branch)
1285
for parent_id in self.get_parent_ids():
1286
new_branch.fetch(self.branch, parent_id)
1287
tree_transport = self.controldir.root_transport.clone(sub_path)
1288
if tree_transport.base != branch_transport.base:
1289
tree_bzrdir = format.initialize_on_transport(tree_transport)
1290
tree_bzrdir.set_branch_reference(new_branch)
1292
tree_bzrdir = branch_bzrdir
1293
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1294
wt.set_parent_ids(self.get_parent_ids())
1297
def _get_check_refs(self):
1298
"""Return the references needed to perform a check of this tree.
1300
The default implementation returns no refs, and is only suitable for
1301
trees that have no local caching and can commit on ghosts at any time.
1303
:seealso: breezy.check for details about check_refs.
1307
def copy_content_into(self, tree, revision_id=None):
1308
"""Copy the current content and user files of this tree into tree."""
1309
with self.lock_read():
1310
if revision_id is None:
1311
merge.transform_tree(tree, self)
1313
# TODO now merge from tree.last_revision to revision (to
1314
# preserve user local changes)
1316
other_tree = self.revision_tree(revision_id)
1317
except errors.NoSuchRevision:
1318
other_tree = self.branch.repository.revision_tree(
1321
merge.transform_tree(tree, other_tree)
1322
if revision_id == _mod_revision.NULL_REVISION:
1325
new_parents = [revision_id]
1326
tree.set_parent_ids(new_parents)
1328
def reference_parent(self, path, possible_transports=None):
1329
remote_url = self.get_reference_info(path)
1330
if remote_url is None:
1331
trace.warning("Unable to find submodule info for %s", path)
1333
return _mod_branch.Branch.open(remote_url, possible_transports=possible_transports)
1335
def get_reference_info(self, path):
1336
submodule_info = self._submodule_info()
1337
info = submodule_info.get(path.encode('utf-8'))
1340
return info[0].decode('utf-8')
1342
def set_reference_info(self, tree_path, branch_location):
1343
path = self.abspath('.gitmodules')
1345
config = GitConfigFile.from_path(path)
1346
except EnvironmentError as e:
1347
if e.errno == errno.ENOENT:
1348
config = GitConfigFile()
1351
section = (b'submodule', tree_path.encode('utf-8'))
1352
if branch_location is None:
1358
branch_location = urlutils.join(
1359
urlutils.strip_segment_parameters(self.branch.user_url),
1363
b'path', tree_path.encode('utf-8'))
1366
b'url', branch_location.encode('utf-8'))
1367
config.write_to_path(path)
1368
self.add('.gitmodules')
1371
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1373
_tree_class = GitWorkingTree
1375
supports_versioned_directories = False
1377
supports_setting_file_ids = False
1379
supports_store_uncommitted = False
1381
supports_leftmost_parent_id_as_ghost = False
1383
supports_righthand_parent_id_as_ghost = False
1385
requires_normalized_unicode_filenames = True
1387
supports_merge_modified = False
1389
ignore_filename = ".gitignore"
1392
def _matchingcontroldir(self):
1393
from .dir import LocalGitControlDirFormat
1394
return LocalGitControlDirFormat()
1396
def get_format_description(self):
1397
return "Git Working Tree"
1399
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1400
accelerator_tree=None, hardlink=False):
1401
"""See WorkingTreeFormat.initialize()."""
1402
if not isinstance(a_controldir, LocalGitDir):
1403
raise errors.IncompatibleFormat(self, a_controldir)
1404
branch = a_controldir.open_branch(nascent_ok=True)
1405
if revision_id is not None:
1406
branch.set_last_revision(revision_id)
1407
wt = GitWorkingTree(
1408
a_controldir, a_controldir.open_repository(), branch)
1409
for hook in MutableTree.hooks['post_build_tree']: