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].decode('utf-8'), '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)
818
ie = fk_entries[kind]()
822
yield (posixpath.relpath(path, from_dir),
823
("I" if self.is_ignored(path) else "?"), kind, ie)
825
def all_file_ids(self):
826
raise errors.UnsupportedOperation(self.all_file_ids, self)
828
def all_versioned_paths(self):
829
with self.lock_read():
831
for path in self.index:
832
if self.mapping.is_special_file(path):
834
path = path.decode("utf-8")
837
path = posixpath.dirname(path).strip("/")
843
def iter_child_entries(self, path):
844
encoded_path = path.encode('utf-8')
845
with self.lock_read():
846
parent_id = self.path2id(path)
848
for item_path, value in self.index.iteritems():
849
decoded_item_path = item_path.decode('utf-8')
850
if self.mapping.is_special_file(item_path):
852
if not osutils.is_inside(path, decoded_item_path):
855
subpath = posixpath.relpath(decoded_item_path, path)
857
dirname = subpath.split('/', 1)[0]
858
file_ie = self._get_dir_ie(
859
posixpath.join(path, dirname), parent_id)
861
(unused_parent, name) = posixpath.split(decoded_item_path)
862
file_ie = self._get_file_ie(
863
name, decoded_item_path, value, parent_id)
865
if not found_any and path != u'':
866
raise errors.NoSuchFile(path)
869
with self.lock_read():
870
conflicts = _mod_conflicts.ConflictList()
871
for item_path, value in self.index.iteritems():
872
if value.flags & FLAG_STAGEMASK:
873
conflicts.append(_mod_conflicts.TextConflict(
874
item_path.decode('utf-8')))
877
def set_conflicts(self, conflicts):
879
for conflict in conflicts:
880
if conflict.typestring in ('text conflict', 'contents conflict'):
881
by_path.add(conflict.path.encode('utf-8'))
883
raise errors.UnsupportedOperation(self.set_conflicts, self)
884
with self.lock_tree_write():
885
for path in self.index:
886
self._set_conflicted(path, path in by_path)
888
def _set_conflicted(self, path, conflicted):
889
trace.mutter('change conflict: %r -> %r', path, conflicted)
890
value = self.index[path]
891
self._index_dirty = True
893
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
895
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
897
def add_conflicts(self, new_conflicts):
898
with self.lock_tree_write():
899
for conflict in new_conflicts:
900
if conflict.typestring in ('text conflict',
901
'contents conflict'):
903
self._set_conflicted(
904
conflict.path.encode('utf-8'), True)
906
raise errors.UnsupportedOperation(
907
self.add_conflicts, self)
909
raise errors.UnsupportedOperation(self.add_conflicts, self)
911
def walkdirs(self, prefix=""):
912
"""Walk the directories of this tree.
914
returns a generator which yields items in the form:
915
((curren_directory_path, fileid),
916
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
919
This API returns a generator, which is only valid during the current
920
tree transaction - within a single lock_read or lock_write duration.
922
If the tree is not locked, it may cause an error to be raised,
923
depending on the tree implementation.
925
from bisect import bisect_left
927
disk_top = self.abspath(prefix)
928
if disk_top.endswith('/'):
929
disk_top = disk_top[:-1]
930
top_strip_len = len(disk_top) + 1
931
inventory_iterator = self._walkdirs(prefix)
932
disk_iterator = osutils.walkdirs(disk_top, prefix)
934
current_disk = next(disk_iterator)
935
disk_finished = False
937
if not (e.errno == errno.ENOENT
938
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
943
current_inv = next(inventory_iterator)
945
except StopIteration:
948
while not inv_finished or not disk_finished:
950
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
951
cur_disk_dir_content) = current_disk
953
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
954
cur_disk_dir_content) = ((None, None), None)
955
if not disk_finished:
956
# strip out .bzr dirs
957
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
958
and len(cur_disk_dir_content) > 0):
959
# osutils.walkdirs can be made nicer -
960
# yield the path-from-prefix rather than the pathjoined
962
bzrdir_loc = bisect_left(cur_disk_dir_content,
964
if (bzrdir_loc < len(cur_disk_dir_content) and
965
self.controldir.is_control_filename(
966
cur_disk_dir_content[bzrdir_loc][0])):
967
# we dont yield the contents of, or, .bzr itself.
968
del cur_disk_dir_content[bzrdir_loc]
970
# everything is unknown
973
# everything is missing
976
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
977
- (current_inv[0][0] < cur_disk_dir_relpath))
979
# disk is before inventory - unknown
980
dirblock = [(relpath, basename, kind, stat, None, None) for
981
relpath, basename, kind, stat, top_path in
982
cur_disk_dir_content]
983
yield (cur_disk_dir_relpath, None), dirblock
985
current_disk = next(disk_iterator)
986
except StopIteration:
989
# inventory is before disk - missing.
990
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
991
for relpath, basename, dkind, stat, fileid, kind in
993
yield (current_inv[0][0], current_inv[0][1]), dirblock
995
current_inv = next(inventory_iterator)
996
except StopIteration:
999
# versioned present directory
1000
# merge the inventory and disk data together
1002
for relpath, subiterator in itertools.groupby(sorted(
1003
current_inv[1] + cur_disk_dir_content,
1004
key=operator.itemgetter(0)), operator.itemgetter(1)):
1005
path_elements = list(subiterator)
1006
if len(path_elements) == 2:
1007
inv_row, disk_row = path_elements
1008
# versioned, present file
1009
dirblock.append((inv_row[0],
1010
inv_row[1], disk_row[2],
1011
disk_row[3], inv_row[4],
1013
elif len(path_elements[0]) == 5:
1016
(path_elements[0][0], path_elements[0][1],
1017
path_elements[0][2], path_elements[0][3],
1019
elif len(path_elements[0]) == 6:
1020
# versioned, absent file.
1022
(path_elements[0][0], path_elements[0][1],
1023
'unknown', None, path_elements[0][4],
1024
path_elements[0][5]))
1026
raise NotImplementedError('unreachable code')
1027
yield current_inv[0], dirblock
1029
current_inv = next(inventory_iterator)
1030
except StopIteration:
1033
current_disk = next(disk_iterator)
1034
except StopIteration:
1035
disk_finished = True
1037
def _walkdirs(self, prefix=u""):
1040
prefix = prefix.encode('utf-8')
1041
per_dir = defaultdict(set)
1043
per_dir[(u'', self.path2id(''))] = set()
1045
def add_entry(path, kind):
1046
if path == b'' or not path.startswith(prefix):
1048
(dirname, child_name) = posixpath.split(path)
1049
add_entry(dirname, 'directory')
1050
dirname = dirname.decode("utf-8")
1051
dir_file_id = self.path2id(dirname)
1052
if not isinstance(value, tuple) or len(value) != 10:
1053
raise ValueError(value)
1054
per_dir[(dirname, dir_file_id)].add(
1055
(path.decode("utf-8"), child_name.decode("utf-8"),
1057
self.path2id(path.decode("utf-8")),
1059
with self.lock_read():
1060
for path, value in self.index.iteritems():
1061
if self.mapping.is_special_file(path):
1063
if not path.startswith(prefix):
1065
add_entry(path, mode_kind(value.mode))
1066
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1068
def get_shelf_manager(self):
1069
raise workingtree.ShelvingUnsupported()
1071
def store_uncommitted(self):
1072
raise errors.StoringUncommittedNotSupported(self)
1074
def apply_inventory_delta(self, changes):
1075
for (old_path, new_path, file_id, ie) in changes:
1076
if old_path is not None:
1077
(index, old_subpath) = self._lookup_index(
1078
old_path.encode('utf-8'))
1080
self._index_del_entry(index, old_subpath)
1084
self._versioned_dirs = None
1085
if new_path is not None and ie.kind != 'directory':
1086
if ie.kind == 'tree-reference':
1087
self._index_add_entry(
1089
reference_revision=ie.reference_revision)
1091
self._index_add_entry(new_path, ie.kind)
1094
def annotate_iter(self, path,
1095
default_revision=_mod_revision.CURRENT_REVISION):
1096
"""See Tree.annotate_iter
1098
This implementation will use the basis tree implementation if possible.
1099
Lines not in the basis are attributed to CURRENT_REVISION
1101
If there are pending merges, lines added by those merges will be
1102
incorrectly attributed to CURRENT_REVISION (but after committing, the
1103
attribution will be correct).
1105
with self.lock_read():
1106
maybe_file_parent_keys = []
1107
for parent_id in self.get_parent_ids():
1109
parent_tree = self.revision_tree(parent_id)
1110
except errors.NoSuchRevisionInTree:
1111
parent_tree = self.branch.repository.revision_tree(
1113
with parent_tree.lock_read():
1114
# TODO(jelmer): Use rename/copy tracker to find path name
1118
kind = parent_tree.kind(parent_path)
1119
except errors.NoSuchFile:
1122
# Note: this is slightly unnecessary, because symlinks
1123
# and directories have a "text" which is the empty
1124
# text, and we know that won't mess up annotations. But
1129
parent_tree.get_file_revision(parent_path))
1130
if parent_text_key not in maybe_file_parent_keys:
1131
maybe_file_parent_keys.append(parent_text_key)
1132
# Now we have the parents of this content
1133
from breezy.annotate import Annotator
1134
from .annotate import AnnotateProvider
1135
annotate_provider = AnnotateProvider(
1136
self.branch.repository._file_change_scanner)
1137
annotator = Annotator(annotate_provider)
1139
from breezy.graph import Graph
1140
graph = Graph(annotate_provider)
1141
heads = graph.heads(maybe_file_parent_keys)
1142
file_parent_keys = []
1143
for key in maybe_file_parent_keys:
1145
file_parent_keys.append(key)
1147
text = self.get_file_text(path)
1148
this_key = (path, default_revision)
1149
annotator.add_special_text(this_key, file_parent_keys, text)
1150
annotations = [(key[-1], line)
1151
for key, line in annotator.annotate_flat(this_key)]
1154
def _rename_one(self, from_rel, to_rel):
1155
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1157
def _build_checkout_with_index(self):
1158
build_index_from_tree(
1159
self.user_transport.local_abspath('.'),
1160
self.control_transport.local_abspath("index"),
1163
if self.branch.head is None
1164
else self.store[self.branch.head].tree,
1165
honor_filemode=self._supports_executable())
1167
def reset_state(self, revision_ids=None):
1168
"""Reset the state of the working tree.
1170
This does a hard-reset to a last-known-good state. This is a way to
1171
fix if something got corrupted (like the .git/index file)
1173
with self.lock_tree_write():
1174
if revision_ids is not None:
1175
self.set_parent_ids(revision_ids)
1177
self._index_dirty = True
1178
if self.branch.head is not None:
1179
for entry in self.store.iter_tree_contents(
1180
self.store[self.branch.head].tree):
1181
if not validate_path(entry.path):
1184
if S_ISGITLINK(entry.mode):
1185
pass # TODO(jelmer): record and return submodule paths
1187
# Let's at least try to use the working tree file:
1189
st = self._lstat(self.abspath(
1190
entry.path.decode('utf-8')))
1192
# But if it doesn't exist, we'll make something up.
1193
obj = self.store[entry.sha]
1194
st = os.stat_result((entry.mode, 0, 0, 0,
1196
obj.as_raw_string()), 0,
1198
(index, subpath) = self._lookup_index(entry.path)
1199
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1201
def _update_git_tree(self, old_revision, new_revision, change_reporter=None,
1203
basis_tree = self.revision_tree(old_revision)
1204
if new_revision != old_revision:
1205
with basis_tree.lock_read():
1206
new_basis_tree = self.branch.basis_tree()
1212
change_reporter=change_reporter,
1213
show_base=show_base)
1215
def pull(self, source, overwrite=False, stop_revision=None,
1216
change_reporter=None, possible_transports=None, local=False,
1217
show_base=False, tag_selector=None):
1218
with self.lock_write(), source.lock_read():
1219
old_revision = self.branch.last_revision()
1220
count = self.branch.pull(source, overwrite, stop_revision,
1221
possible_transports=possible_transports,
1222
local=local, tag_selector=tag_selector)
1223
self._update_git_tree(
1224
old_revision=old_revision,
1225
new_revision=self.branch.last_revision(),
1226
change_reporter=change_reporter,
1227
show_base=show_base)
1230
def add_reference(self, sub_tree):
1231
"""Add a TreeReference to the tree, pointing at sub_tree.
1233
:param sub_tree: subtree to add.
1235
with self.lock_tree_write():
1237
sub_tree_path = self.relpath(sub_tree.basedir)
1238
except errors.PathNotChild:
1239
raise BadReferenceTarget(
1240
self, sub_tree, 'Target not inside tree.')
1242
self._add([sub_tree_path], [None], ['tree-reference'])
1244
def _read_submodule_head(self, path):
1245
return read_submodule_head(self.abspath(path))
1247
def get_reference_revision(self, path, branch=None):
1248
hexsha = self._read_submodule_head(path)
1250
return _mod_revision.NULL_REVISION
1251
return self.branch.lookup_foreign_revision_id(hexsha)
1253
def get_nested_tree(self, path):
1254
return workingtree.WorkingTree.open(self.abspath(path))
1256
def _directory_is_tree_reference(self, relpath):
1257
# as a special case, if a directory contains control files then
1258
# it's a tree reference, except that the root of the tree is not
1259
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1261
def extract(self, sub_path, format=None):
1262
"""Extract a subtree from this tree.
1264
A new branch will be created, relative to the path for this tree.
1267
segments = osutils.splitpath(path)
1268
transport = self.branch.controldir.root_transport
1269
for name in segments:
1270
transport = transport.clone(name)
1271
transport.ensure_base()
1274
with self.lock_tree_write():
1276
branch_transport = mkdirs(sub_path)
1278
format = self.controldir.cloning_metadir()
1279
branch_transport.ensure_base()
1280
branch_bzrdir = format.initialize_on_transport(branch_transport)
1282
repo = branch_bzrdir.find_repository()
1283
except errors.NoRepositoryPresent:
1284
repo = branch_bzrdir.create_repository()
1285
if not repo.supports_rich_root():
1286
raise errors.RootNotRich()
1287
new_branch = branch_bzrdir.create_branch()
1288
new_branch.pull(self.branch)
1289
for parent_id in self.get_parent_ids():
1290
new_branch.fetch(self.branch, parent_id)
1291
tree_transport = self.controldir.root_transport.clone(sub_path)
1292
if tree_transport.base != branch_transport.base:
1293
tree_bzrdir = format.initialize_on_transport(tree_transport)
1294
tree_bzrdir.set_branch_reference(new_branch)
1296
tree_bzrdir = branch_bzrdir
1297
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1298
wt.set_parent_ids(self.get_parent_ids())
1301
def _get_check_refs(self):
1302
"""Return the references needed to perform a check of this tree.
1304
The default implementation returns no refs, and is only suitable for
1305
trees that have no local caching and can commit on ghosts at any time.
1307
:seealso: breezy.check for details about check_refs.
1311
def copy_content_into(self, tree, revision_id=None):
1312
"""Copy the current content and user files of this tree into tree."""
1313
with self.lock_read():
1314
if revision_id is None:
1315
merge.transform_tree(tree, self)
1317
# TODO now merge from tree.last_revision to revision (to
1318
# preserve user local changes)
1320
other_tree = self.revision_tree(revision_id)
1321
except errors.NoSuchRevision:
1322
other_tree = self.branch.repository.revision_tree(
1325
merge.transform_tree(tree, other_tree)
1326
if revision_id == _mod_revision.NULL_REVISION:
1329
new_parents = [revision_id]
1330
tree.set_parent_ids(new_parents)
1332
def reference_parent(self, path, possible_transports=None):
1333
remote_url = self.get_reference_info(path)
1334
if remote_url is None:
1335
trace.warning("Unable to find submodule info for %s", path)
1337
return _mod_branch.Branch.open(remote_url, possible_transports=possible_transports)
1339
def get_reference_info(self, path):
1340
submodule_info = self._submodule_info()
1341
info = submodule_info.get(path.encode('utf-8'))
1344
return info[0].decode('utf-8')
1346
def set_reference_info(self, tree_path, branch_location):
1347
path = self.abspath('.gitmodules')
1349
config = GitConfigFile.from_path(path)
1350
except EnvironmentError as e:
1351
if e.errno == errno.ENOENT:
1352
config = GitConfigFile()
1355
section = (b'submodule', tree_path.encode('utf-8'))
1356
if branch_location is None:
1362
branch_location = urlutils.join(
1363
urlutils.strip_segment_parameters(self.branch.user_url),
1367
b'path', tree_path.encode('utf-8'))
1370
b'url', branch_location.encode('utf-8'))
1371
config.write_to_path(path)
1372
self.add('.gitmodules')
1375
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1377
_tree_class = GitWorkingTree
1379
supports_versioned_directories = False
1381
supports_setting_file_ids = False
1383
supports_store_uncommitted = False
1385
supports_leftmost_parent_id_as_ghost = False
1387
supports_righthand_parent_id_as_ghost = False
1389
requires_normalized_unicode_filenames = True
1391
supports_merge_modified = False
1393
ignore_filename = ".gitignore"
1396
def _matchingcontroldir(self):
1397
from .dir import LocalGitControlDirFormat
1398
return LocalGitControlDirFormat()
1400
def get_format_description(self):
1401
return "Git Working Tree"
1403
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1404
accelerator_tree=None, hardlink=False):
1405
"""See WorkingTreeFormat.initialize()."""
1406
if not isinstance(a_controldir, LocalGitDir):
1407
raise errors.IncompatibleFormat(self, a_controldir)
1408
branch = a_controldir.open_branch(nascent_ok=True)
1409
if revision_id is not None:
1410
branch.set_last_revision(revision_id)
1411
wt = GitWorkingTree(
1412
a_controldir, a_controldir.open_repository(), branch)
1413
for hook in MutableTree.hooks['post_build_tree']: