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.config import ConfigFile as GitConfigFile
29
from dulwich.file import GitFile, FileLocked
30
from dulwich.index import (
33
build_index_from_tree,
34
index_entry_from_path,
35
index_entry_from_stat,
41
from dulwich.object_store import (
44
from dulwich.objects import (
53
branch as _mod_branch,
54
conflicts as _mod_conflicts,
56
controldir as _mod_controldir,
62
revision as _mod_revision,
64
transport as _mod_transport,
69
from ..decorators import (
72
from ..mutabletree import (
76
from ..sixish import text_type
85
from .mapping import (
90
class GitWorkingTree(MutableGitIndexTree, workingtree.WorkingTree):
91
"""A Git working tree."""
93
def __init__(self, controldir, repo, branch):
94
MutableGitIndexTree.__init__(self)
95
basedir = controldir.root_transport.local_abspath('.')
96
self.basedir = osutils.realpath(basedir)
97
self.controldir = controldir
98
self.repository = repo
99
self.store = self.repository._git.object_store
100
self.mapping = self.repository.get_mapping()
101
self._branch = branch
102
self._transport = self.repository._git._controltransport
103
self._format = GitWorkingTreeFormat()
105
self._index_file = None
106
self.views = self._make_views()
107
self._rules_searcher = None
108
self._detect_case_handling()
111
def supports_tree_reference(self):
114
def supports_rename_tracking(self):
117
def _read_index(self):
118
self.index = Index(self.control_transport.local_abspath('index'))
119
self._index_dirty = False
121
def _get_submodule_index(self, relpath):
122
if not isinstance(relpath, bytes):
123
raise TypeError(relpath)
125
info = self._submodule_info()[relpath]
127
index_path = os.path.join(self.basedir, relpath.decode('utf-8'), '.git', 'index')
129
index_path = self.control_transport.local_abspath(
130
posixpath.join('modules', info[1].decode('utf-8'), 'index'))
131
return Index(index_path)
134
"""Lock the repository for read operations.
136
:return: A breezy.lock.LogicalLockResult.
138
if not self._lock_mode:
139
self._lock_mode = 'r'
143
self._lock_count += 1
144
self.branch.lock_read()
145
return lock.LogicalLockResult(self.unlock)
147
def _lock_write_tree(self):
148
if not self._lock_mode:
149
self._lock_mode = 'w'
152
self._index_file = GitFile(
153
self.control_transport.local_abspath('index'), 'wb')
155
raise errors.LockContention('index')
157
elif self._lock_mode == 'r':
158
raise errors.ReadOnlyError(self)
160
self._lock_count += 1
162
def lock_tree_write(self):
163
self.branch.lock_read()
165
self._lock_write_tree()
166
return lock.LogicalLockResult(self.unlock)
167
except BaseException:
171
def lock_write(self, token=None):
172
self.branch.lock_write()
174
self._lock_write_tree()
175
return lock.LogicalLockResult(self.unlock)
176
except BaseException:
181
return self._lock_count >= 1
183
def get_physical_lock_status(self):
186
def break_lock(self):
188
self.control_transport.delete('index.lock')
189
except errors.NoSuchFile:
191
self.branch.break_lock()
193
@only_raises(errors.LockNotHeld, errors.LockBroken)
195
if not self._lock_count:
196
return lock.cant_unlock_not_held(self)
199
self._lock_count -= 1
200
if self._lock_count > 0:
202
if self._index_file is not None:
203
if self._index_dirty:
204
self._flush(self._index_file)
205
self._index_file.close()
207
# Something else already triggered a write of the index
208
# file by calling .flush()
209
self._index_file.abort()
210
self._index_file = None
211
self._lock_mode = None
219
def _detect_case_handling(self):
221
self._transport.stat(".git/cOnFiG")
222
except errors.NoSuchFile:
223
self.case_sensitive = True
225
self.case_sensitive = False
227
def get_transform(self, pb=None):
228
from ..transform import TreeTransform
229
return TreeTransform(self, pb=pb)
231
def merge_modified(self):
234
def set_merge_modified(self, modified_hashes):
235
raise errors.UnsupportedOperation(self.set_merge_modified, self)
237
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
238
self.set_parent_ids([p for p, t in parents_list])
240
def _set_merges_from_parent_ids(self, rhs_parent_ids):
242
merges = [self.branch.lookup_bzr_revision_id(
243
revid)[0] for revid in rhs_parent_ids]
244
except errors.NoSuchRevision as e:
245
raise errors.GhostRevisionUnusableHere(e.revision)
247
self.control_transport.put_bytes(
248
'MERGE_HEAD', b'\n'.join(merges),
249
mode=self.controldir._get_file_mode())
252
self.control_transport.delete('MERGE_HEAD')
253
except errors.NoSuchFile:
256
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
257
"""Set the parent ids to revision_ids.
259
See also set_parent_trees. This api will try to retrieve the tree data
260
for each element of revision_ids from the trees repository. If you have
261
tree data already available, it is more efficient to use
262
set_parent_trees rather than set_parent_ids. set_parent_ids is however
263
an easier API to use.
265
:param revision_ids: The revision_ids to set as the parent ids of this
266
working tree. Any of these may be ghosts.
268
with self.lock_tree_write():
269
self._check_parents_for_ghosts(
270
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
271
for revision_id in revision_ids:
272
_mod_revision.check_not_reserved_id(revision_id)
274
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
276
if len(revision_ids) > 0:
277
self.set_last_revision(revision_ids[0])
279
self.set_last_revision(_mod_revision.NULL_REVISION)
281
self._set_merges_from_parent_ids(revision_ids[1:])
283
def get_parent_ids(self):
284
"""See Tree.get_parent_ids.
286
This implementation reads the pending merges list and last_revision
287
value and uses that to decide what the parents list should be.
289
last_rev = _mod_revision.ensure_null(self._last_revision())
290
if _mod_revision.NULL_REVISION == last_rev:
295
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
296
except errors.NoSuchFile:
299
for l in osutils.split_lines(merges_bytes):
300
revision_id = l.rstrip(b'\n')
302
self.branch.lookup_foreign_revision_id(revision_id))
305
def check_state(self):
306
"""Check that the working state is/isn't valid."""
309
def remove(self, files, verbose=False, to_file=None, keep_files=True,
311
"""Remove nominated files from the working tree metadata.
313
:param files: File paths relative to the basedir.
314
:param keep_files: If true, the files will also be kept.
315
:param force: Delete files and directories, even if they are changed
316
and even if the directories are not empty.
318
if not isinstance(files, list):
324
def backup(file_to_backup):
325
abs_path = self.abspath(file_to_backup)
326
backup_name = self.controldir._available_backup_name(
328
osutils.rename(abs_path, self.abspath(backup_name))
329
return "removed %s (but kept a copy: %s)" % (
330
file_to_backup, backup_name)
332
# Sort needed to first handle directory content before the directory
337
def recurse_directory_to_add_files(directory):
338
# Recurse directory and add all files
339
# so we can check if they have changed.
340
for parent_info, file_infos in self.walkdirs(directory):
341
for relpath, basename, kind, lstat, fileid, kind in file_infos:
342
# Is it versioned or ignored?
343
if self.is_versioned(relpath):
344
# Add nested content for deletion.
345
all_files.add(relpath)
347
# Files which are not versioned
348
# should be treated as unknown.
349
files_to_backup.append(relpath)
351
with self.lock_tree_write():
352
for filepath in files:
353
# Get file name into canonical form.
354
abspath = self.abspath(filepath)
355
filepath = self.relpath(abspath)
358
all_files.add(filepath)
359
recurse_directory_to_add_files(filepath)
361
files = list(all_files)
364
return # nothing to do
366
# Sort needed to first handle directory content before the
368
files.sort(reverse=True)
370
# Bail out if we are going to delete files we shouldn't
371
if not keep_files and not force:
372
for change in self.iter_changes(
373
self.basis_tree(), include_unchanged=True,
374
require_versioned=False, want_unversioned=True,
375
specific_files=files):
376
if change.versioned[0] is False:
377
# The record is unknown or newly added
378
files_to_backup.append(change.path[1])
379
files_to_backup.extend(
380
osutils.parent_directories(change.path[1]))
381
elif (change.changed_content and (change.kind[1] is not None)
382
and osutils.is_inside_any(files, change.path[1])):
383
# Versioned and changed, but not deleted, and still
384
# in one of the dirs to be deleted.
385
files_to_backup.append(change.path[1])
386
files_to_backup.extend(
387
osutils.parent_directories(change.path[1]))
395
except errors.NoSuchFile:
398
abs_path = self.abspath(f)
400
# having removed it, it must be either ignored or unknown
401
if self.is_ignored(f):
405
kind_ch = osutils.kind_marker(kind)
406
to_file.write(new_status + ' ' + f + kind_ch + '\n')
408
message = "%s does not exist" % (f, )
411
if f in files_to_backup and not force:
414
if kind == 'directory':
415
osutils.rmtree(abs_path)
417
osutils.delete_any(abs_path)
418
message = "deleted %s" % (f,)
420
message = "removed %s" % (f,)
421
self._unversion_path(f)
423
# print only one message (if any) per file.
424
if message is not None:
426
self._versioned_dirs = None
428
def smart_add(self, file_list, recurse=True, action=None, save=True):
432
# expand any symlinks in the directory part, while leaving the
434
# only expanding if symlinks are supported avoids windows path bugs
435
if self.supports_symlinks():
436
file_list = list(map(osutils.normalizepath, file_list))
438
conflicts_related = set()
439
for c in self.conflicts():
440
conflicts_related.update(c.associated_filenames())
446
def call_action(filepath, kind):
449
if action is not None:
450
parent_path = posixpath.dirname(filepath)
451
parent_id = self.path2id(parent_path)
452
parent_ie = self._get_dir_ie(parent_path, parent_id)
453
file_id = action(self, parent_ie, filepath, kind)
454
if file_id is not None:
455
raise workingtree.SettingFileIdUnsupported()
457
with self.lock_tree_write():
458
for filepath in osutils.canonical_relpaths(
459
self.basedir, file_list):
460
filepath, can_access = osutils.normalized_filename(filepath)
462
raise errors.InvalidNormalization(filepath)
464
abspath = self.abspath(filepath)
465
kind = osutils.file_kind(abspath)
466
if kind in ("file", "symlink"):
467
(index, subpath) = self._lookup_index(
468
filepath.encode('utf-8'))
472
call_action(filepath, kind)
474
self._index_add_entry(filepath, kind)
475
added.append(filepath)
476
elif kind == "directory":
477
(index, subpath) = self._lookup_index(
478
filepath.encode('utf-8'))
479
if subpath not in index:
480
call_action(filepath, kind)
482
user_dirs.append(filepath)
484
raise errors.BadFileKindError(filename=abspath, kind=kind)
485
for user_dir in user_dirs:
486
abs_user_dir = self.abspath(user_dir)
489
transport = _mod_transport.get_transport_from_path(
491
_mod_controldir.ControlDirFormat.find_format(transport)
493
except errors.NotBranchError:
495
except errors.UnsupportedFormatError:
500
trace.warning('skipping nested tree %r', abs_user_dir)
503
for name in os.listdir(abs_user_dir):
504
subp = os.path.join(user_dir, name)
505
if (self.is_control_filename(subp) or
506
self.mapping.is_special_file(subp)):
508
ignore_glob = self.is_ignored(subp)
509
if ignore_glob is not None:
510
ignored.setdefault(ignore_glob, []).append(subp)
512
abspath = self.abspath(subp)
513
kind = osutils.file_kind(abspath)
514
if kind == "directory":
515
user_dirs.append(subp)
517
(index, subpath) = self._lookup_index(
518
subp.encode('utf-8'))
522
if subp in conflicts_related:
524
call_action(subp, kind)
526
self._index_add_entry(subp, kind)
528
return added, ignored
530
def has_filename(self, filename):
531
return osutils.lexists(self.abspath(filename))
533
def _iter_files_recursive(self, from_dir=None, include_dirs=False,
534
recurse_nested=False):
537
if not isinstance(from_dir, text_type):
538
raise TypeError(from_dir)
539
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
540
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
541
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
542
if self.controldir.is_control_filename(
543
dir_relpath.decode(osutils._fs_enc)):
545
for name in list(dirnames):
546
if self.controldir.is_control_filename(
547
name.decode(osutils._fs_enc)):
548
dirnames.remove(name)
550
relpath = os.path.join(dir_relpath, name)
551
if not recurse_nested and self._directory_is_tree_reference(relpath.decode(osutils._fs_enc)):
552
dirnames.remove(name)
555
yield relpath.decode(osutils._fs_enc)
556
except UnicodeDecodeError:
557
raise errors.BadFilenameEncoding(
558
relpath, osutils._fs_enc)
559
if not self.is_versioned(relpath.decode(osutils._fs_enc)):
560
dirnames.remove(name)
561
for name in filenames:
562
if self.mapping.is_special_file(name):
564
if self.controldir.is_control_filename(
565
name.decode(osutils._fs_enc, 'replace')):
567
yp = os.path.join(dir_relpath, name)
569
yield yp.decode(osutils._fs_enc)
570
except UnicodeDecodeError:
571
raise errors.BadFilenameEncoding(
575
"""Yield all unversioned files in this WorkingTree.
577
with self.lock_read():
579
[p.decode('utf-8') for p, i in self._recurse_index_entries()])
580
all_paths = set(self._iter_files_recursive(include_dirs=False))
581
return iter(all_paths - index_paths)
583
def _gather_kinds(self, files, kinds):
584
"""See MutableTree._gather_kinds."""
585
with self.lock_tree_write():
586
for pos, f in enumerate(files):
587
if kinds[pos] is None:
588
fullpath = osutils.normpath(self.abspath(f))
590
kind = osutils.file_kind(fullpath)
592
if e.errno == errno.ENOENT:
593
raise errors.NoSuchFile(fullpath)
594
if f != '' and self._directory_is_tree_reference(f):
595
kind = 'tree-reference'
599
if self._lock_mode != 'w':
600
raise errors.NotWriteLocked(self)
601
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
602
# already in use and GitFile doesn't allow overriding the lock file
604
f = open(self.control_transport.local_abspath('index'), 'wb')
605
# Note that _flush will close the file
611
write_index_dict(shaf, self.index)
613
except BaseException:
616
self._index_dirty = False
618
def get_file_mtime(self, path):
619
"""See Tree.get_file_mtime."""
621
return self._lstat(path).st_mtime
623
if e.errno == errno.ENOENT:
624
raise errors.NoSuchFile(path)
627
def is_ignored(self, filename):
628
r"""Check whether the filename matches an ignore pattern.
630
If the file is ignored, returns the pattern which caused it to
631
be ignored, otherwise None. So this can simply be used as a
632
boolean if desired."""
633
if getattr(self, '_global_ignoreglobster', None) is None:
635
ignore_globs.update(ignores.get_runtime_ignores())
636
ignore_globs.update(ignores.get_user_ignores())
637
self._global_ignoreglobster = globbing.ExceptionGlobster(
639
match = self._global_ignoreglobster.match(filename)
640
if match is not None:
643
if self.kind(filename) == 'directory':
645
except errors.NoSuchFile:
647
filename = filename.lstrip('/')
648
ignore_manager = self._get_ignore_manager()
649
ps = list(ignore_manager.find_matching(filename))
652
if not ps[-1].is_exclude:
656
def _get_ignore_manager(self):
657
ignoremanager = getattr(self, '_ignoremanager', None)
658
if ignoremanager is not None:
661
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
662
self._ignoremanager = ignore_manager
663
return ignore_manager
665
def _flush_ignore_list_cache(self):
666
self._ignoremanager = None
668
def set_last_revision(self, revid):
669
if _mod_revision.is_null(revid):
670
self.branch.set_last_revision_info(0, revid)
672
_mod_revision.check_not_reserved_id(revid)
674
self.branch.generate_revision_history(revid)
675
except errors.NoSuchRevision:
676
raise errors.GhostRevisionUnusableHere(revid)
678
def _reset_data(self):
681
def get_file_verifier(self, path, stat_value=None):
682
with self.lock_read():
683
(index, subpath) = self._lookup_index(path.encode('utf-8'))
685
return ("GIT", index[subpath].sha)
687
if self._has_dir(path):
689
raise errors.NoSuchFile(path)
691
def get_file_sha1(self, path, stat_value=None):
692
with self.lock_read():
693
if not self.is_versioned(path):
694
raise errors.NoSuchFile(path)
695
abspath = self.abspath(path)
697
return osutils.sha_file_by_name(abspath)
699
if e.errno in (errno.EISDIR, errno.ENOENT):
703
def revision_tree(self, revid):
704
return self.repository.revision_tree(revid)
706
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
707
mode = stat_result.st_mode
708
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
710
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
711
return self.basis_tree().is_executable(path)
713
def stored_kind(self, path):
714
with self.lock_read():
715
encoded_path = path.encode('utf-8')
716
(index, subpath) = self._lookup_index(encoded_path)
718
return mode_kind(index[subpath].mode)
720
# Maybe it's a directory?
721
if self._has_dir(encoded_path):
723
raise errors.NoSuchFile(path)
725
def _lstat(self, path):
726
return os.lstat(self.abspath(path))
728
def _live_entry(self, path):
729
encoded_path = self.abspath(path.decode('utf-8')).encode(
731
return index_entry_from_path(encoded_path)
733
def is_executable(self, path):
734
with self.lock_read():
735
if self._supports_executable():
736
mode = self._lstat(path).st_mode
738
(index, subpath) = self._lookup_index(path.encode('utf-8'))
740
mode = index[subpath].mode
743
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
745
def _is_executable_from_path_and_stat(self, path, stat_result):
746
if self._supports_executable():
747
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
749
return self._is_executable_from_path_and_stat_from_basis(
752
def list_files(self, include_root=False, from_dir=None, recursive=True,
753
recurse_nested=False):
754
if from_dir is None or from_dir == '.':
757
fk_entries = {'directory': tree.TreeDirectory,
758
'file': tree.TreeFile,
759
'symlink': tree.TreeLink,
760
'tree-reference': tree.TreeReference}
761
with self.lock_read():
762
root_ie = self._get_dir_ie(u"", None)
763
if include_root and not from_dir:
764
yield "", "V", root_ie.kind, root_ie
765
dir_ids[u""] = root_ie.file_id
767
path_iterator = sorted(
768
self._iter_files_recursive(
769
from_dir, include_dirs=True,
770
recurse_nested=recurse_nested))
772
encoded_from_dir = self.abspath(from_dir).encode(
774
path_iterator = sorted(
775
[os.path.join(from_dir, name.decode(osutils._fs_enc))
776
for name in os.listdir(encoded_from_dir)
777
if not self.controldir.is_control_filename(
778
name.decode(osutils._fs_enc)) and
779
not self.mapping.is_special_file(
780
name.decode(osutils._fs_enc))])
781
for path in path_iterator:
783
encoded_path = path.encode("utf-8")
784
except UnicodeEncodeError:
785
raise errors.BadFilenameEncoding(
786
path, osutils._fs_enc)
787
(index, index_path) = self._lookup_index(encoded_path)
789
value = index[index_path]
792
kind = self.kind(path)
793
parent, name = posixpath.split(path)
794
for dir_path, dir_ie in self._add_missing_parent_ids(
797
if kind == 'tree-reference' and recurse_nested:
798
ie = self._get_dir_ie(path, self.path2id(path))
799
yield (posixpath.relpath(path, from_dir), 'V', 'directory',
802
if kind == 'directory':
804
if self._has_dir(encoded_path):
805
ie = self._get_dir_ie(path, self.path2id(path))
807
elif self.is_ignored(path):
809
ie = fk_entries[kind]()
812
ie = fk_entries[kind]()
813
yield (posixpath.relpath(path, from_dir), status, kind,
816
if value is not None:
817
ie = self._get_file_ie(name, path, value, dir_ids[parent])
818
yield (posixpath.relpath(path, from_dir), "V", ie.kind, ie)
820
ie = fk_entries[kind]()
821
yield (posixpath.relpath(path, from_dir),
822
("I" if self.is_ignored(path) else "?"), kind, ie)
824
def all_file_ids(self):
825
raise errors.UnsupportedOperation(self.all_file_ids, self)
827
def all_versioned_paths(self):
828
with self.lock_read():
830
for path in self.index:
831
if self.mapping.is_special_file(path):
833
path = path.decode("utf-8")
836
path = posixpath.dirname(path).strip("/")
842
def iter_child_entries(self, path):
843
encoded_path = path.encode('utf-8')
844
with self.lock_read():
845
parent_id = self.path2id(path)
847
for item_path, value in self.index.iteritems():
848
decoded_item_path = item_path.decode('utf-8')
849
if self.mapping.is_special_file(item_path):
851
if not osutils.is_inside(path, decoded_item_path):
854
subpath = posixpath.relpath(decoded_item_path, path)
856
dirname = subpath.split('/', 1)[0]
857
file_ie = self._get_dir_ie(
858
posixpath.join(path, dirname), parent_id)
860
(unused_parent, name) = posixpath.split(decoded_item_path)
861
file_ie = self._get_file_ie(
862
name, decoded_item_path, value, parent_id)
864
if not found_any and path != u'':
865
raise errors.NoSuchFile(path)
868
with self.lock_read():
869
conflicts = _mod_conflicts.ConflictList()
870
for item_path, value in self.index.iteritems():
871
if value.flags & FLAG_STAGEMASK:
872
conflicts.append(_mod_conflicts.TextConflict(
873
item_path.decode('utf-8')))
876
def set_conflicts(self, conflicts):
878
for conflict in conflicts:
879
if conflict.typestring in ('text conflict', 'contents conflict'):
880
by_path.add(conflict.path.encode('utf-8'))
882
raise errors.UnsupportedOperation(self.set_conflicts, self)
883
with self.lock_tree_write():
884
for path in self.index:
885
self._set_conflicted(path, path in by_path)
887
def _set_conflicted(self, path, conflicted):
888
trace.mutter('change conflict: %r -> %r', path, conflicted)
889
value = self.index[path]
890
self._index_dirty = True
892
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
894
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
896
def add_conflicts(self, new_conflicts):
897
with self.lock_tree_write():
898
for conflict in new_conflicts:
899
if conflict.typestring in ('text conflict',
900
'contents conflict'):
902
self._set_conflicted(
903
conflict.path.encode('utf-8'), True)
905
raise errors.UnsupportedOperation(
906
self.add_conflicts, self)
908
raise errors.UnsupportedOperation(self.add_conflicts, self)
910
def walkdirs(self, prefix=""):
911
"""Walk the directories of this tree.
913
returns a generator which yields items in the form:
914
((curren_directory_path, fileid),
915
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
918
This API returns a generator, which is only valid during the current
919
tree transaction - within a single lock_read or lock_write duration.
921
If the tree is not locked, it may cause an error to be raised,
922
depending on the tree implementation.
924
from bisect import bisect_left
926
disk_top = self.abspath(prefix)
927
if disk_top.endswith('/'):
928
disk_top = disk_top[:-1]
929
top_strip_len = len(disk_top) + 1
930
inventory_iterator = self._walkdirs(prefix)
931
disk_iterator = osutils.walkdirs(disk_top, prefix)
933
current_disk = next(disk_iterator)
934
disk_finished = False
936
if not (e.errno == errno.ENOENT
937
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
942
current_inv = next(inventory_iterator)
944
except StopIteration:
947
while not inv_finished or not disk_finished:
949
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
950
cur_disk_dir_content) = current_disk
952
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
953
cur_disk_dir_content) = ((None, None), None)
954
if not disk_finished:
955
# strip out .bzr dirs
956
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
957
and len(cur_disk_dir_content) > 0):
958
# osutils.walkdirs can be made nicer -
959
# yield the path-from-prefix rather than the pathjoined
961
bzrdir_loc = bisect_left(cur_disk_dir_content,
963
if (bzrdir_loc < len(cur_disk_dir_content) and
964
self.controldir.is_control_filename(
965
cur_disk_dir_content[bzrdir_loc][0])):
966
# we dont yield the contents of, or, .bzr itself.
967
del cur_disk_dir_content[bzrdir_loc]
969
# everything is unknown
972
# everything is missing
975
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
976
- (current_inv[0][0] < cur_disk_dir_relpath))
978
# disk is before inventory - unknown
979
dirblock = [(relpath, basename, kind, stat, None, None) for
980
relpath, basename, kind, stat, top_path in
981
cur_disk_dir_content]
982
yield (cur_disk_dir_relpath, None), dirblock
984
current_disk = next(disk_iterator)
985
except StopIteration:
988
# inventory is before disk - missing.
989
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
990
for relpath, basename, dkind, stat, fileid, kind in
992
yield (current_inv[0][0], current_inv[0][1]), dirblock
994
current_inv = next(inventory_iterator)
995
except StopIteration:
998
# versioned present directory
999
# merge the inventory and disk data together
1001
for relpath, subiterator in itertools.groupby(sorted(
1002
current_inv[1] + cur_disk_dir_content,
1003
key=operator.itemgetter(0)), operator.itemgetter(1)):
1004
path_elements = list(subiterator)
1005
if len(path_elements) == 2:
1006
inv_row, disk_row = path_elements
1007
# versioned, present file
1008
dirblock.append((inv_row[0],
1009
inv_row[1], disk_row[2],
1010
disk_row[3], inv_row[4],
1012
elif len(path_elements[0]) == 5:
1015
(path_elements[0][0], path_elements[0][1],
1016
path_elements[0][2], path_elements[0][3],
1018
elif len(path_elements[0]) == 6:
1019
# versioned, absent file.
1021
(path_elements[0][0], path_elements[0][1],
1022
'unknown', None, path_elements[0][4],
1023
path_elements[0][5]))
1025
raise NotImplementedError('unreachable code')
1026
yield current_inv[0], dirblock
1028
current_inv = next(inventory_iterator)
1029
except StopIteration:
1032
current_disk = next(disk_iterator)
1033
except StopIteration:
1034
disk_finished = True
1036
def _walkdirs(self, prefix=u""):
1039
prefix = prefix.encode('utf-8')
1040
per_dir = defaultdict(set)
1042
per_dir[(u'', self.path2id(''))] = set()
1044
def add_entry(path, kind):
1045
if path == b'' or not path.startswith(prefix):
1047
(dirname, child_name) = posixpath.split(path)
1048
add_entry(dirname, 'directory')
1049
dirname = dirname.decode("utf-8")
1050
dir_file_id = self.path2id(dirname)
1051
if not isinstance(value, tuple) or len(value) != 10:
1052
raise ValueError(value)
1053
per_dir[(dirname, dir_file_id)].add(
1054
(path.decode("utf-8"), child_name.decode("utf-8"),
1056
self.path2id(path.decode("utf-8")),
1058
with self.lock_read():
1059
for path, value in self.index.iteritems():
1060
if self.mapping.is_special_file(path):
1062
if not path.startswith(prefix):
1064
add_entry(path, mode_kind(value.mode))
1065
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1067
def get_shelf_manager(self):
1068
raise workingtree.ShelvingUnsupported()
1070
def store_uncommitted(self):
1071
raise errors.StoringUncommittedNotSupported(self)
1073
def apply_inventory_delta(self, changes):
1074
for (old_path, new_path, file_id, ie) in changes:
1075
if old_path is not None:
1076
(index, old_subpath) = self._lookup_index(
1077
old_path.encode('utf-8'))
1079
self._index_del_entry(index, old_subpath)
1083
self._versioned_dirs = None
1084
if new_path is not None and ie.kind != 'directory':
1085
if ie.kind == 'tree-reference':
1086
self._index_add_entry(
1088
reference_revision=ie.reference_revision)
1090
self._index_add_entry(new_path, ie.kind)
1093
def annotate_iter(self, path,
1094
default_revision=_mod_revision.CURRENT_REVISION):
1095
"""See Tree.annotate_iter
1097
This implementation will use the basis tree implementation if possible.
1098
Lines not in the basis are attributed to CURRENT_REVISION
1100
If there are pending merges, lines added by those merges will be
1101
incorrectly attributed to CURRENT_REVISION (but after committing, the
1102
attribution will be correct).
1104
with self.lock_read():
1105
maybe_file_parent_keys = []
1106
for parent_id in self.get_parent_ids():
1108
parent_tree = self.revision_tree(parent_id)
1109
except errors.NoSuchRevisionInTree:
1110
parent_tree = self.branch.repository.revision_tree(
1112
with parent_tree.lock_read():
1113
# TODO(jelmer): Use rename/copy tracker to find path name
1117
kind = parent_tree.kind(parent_path)
1118
except errors.NoSuchFile:
1121
# Note: this is slightly unnecessary, because symlinks
1122
# and directories have a "text" which is the empty
1123
# text, and we know that won't mess up annotations. But
1128
parent_tree.get_file_revision(parent_path))
1129
if parent_text_key not in maybe_file_parent_keys:
1130
maybe_file_parent_keys.append(parent_text_key)
1131
# Now we have the parents of this content
1132
from breezy.annotate import Annotator
1133
from .annotate import AnnotateProvider
1134
annotate_provider = AnnotateProvider(
1135
self.branch.repository._file_change_scanner)
1136
annotator = Annotator(annotate_provider)
1138
from breezy.graph import Graph
1139
graph = Graph(annotate_provider)
1140
heads = graph.heads(maybe_file_parent_keys)
1141
file_parent_keys = []
1142
for key in maybe_file_parent_keys:
1144
file_parent_keys.append(key)
1146
text = self.get_file_text(path)
1147
this_key = (path, default_revision)
1148
annotator.add_special_text(this_key, file_parent_keys, text)
1149
annotations = [(key[-1], line)
1150
for key, line in annotator.annotate_flat(this_key)]
1153
def _rename_one(self, from_rel, to_rel):
1154
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1156
def _build_checkout_with_index(self):
1157
build_index_from_tree(
1158
self.user_transport.local_abspath('.'),
1159
self.control_transport.local_abspath("index"),
1162
if self.branch.head is None
1163
else self.store[self.branch.head].tree,
1164
honor_filemode=self._supports_executable())
1166
def reset_state(self, revision_ids=None):
1167
"""Reset the state of the working tree.
1169
This does a hard-reset to a last-known-good state. This is a way to
1170
fix if something got corrupted (like the .git/index file)
1172
with self.lock_tree_write():
1173
if revision_ids is not None:
1174
self.set_parent_ids(revision_ids)
1176
self._index_dirty = True
1177
if self.branch.head is not None:
1178
for entry in self.store.iter_tree_contents(
1179
self.store[self.branch.head].tree):
1180
if not validate_path(entry.path):
1183
if S_ISGITLINK(entry.mode):
1184
pass # TODO(jelmer): record and return submodule paths
1186
# Let's at least try to use the working tree file:
1188
st = self._lstat(self.abspath(
1189
entry.path.decode('utf-8')))
1191
# But if it doesn't exist, we'll make something up.
1192
obj = self.store[entry.sha]
1193
st = os.stat_result((entry.mode, 0, 0, 0,
1195
obj.as_raw_string()), 0,
1197
(index, subpath) = self._lookup_index(entry.path)
1198
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1200
def _update_git_tree(self, old_revision, new_revision, change_reporter=None,
1202
basis_tree = self.revision_tree(old_revision)
1203
if new_revision != old_revision:
1204
with basis_tree.lock_read():
1205
new_basis_tree = self.branch.basis_tree()
1211
change_reporter=change_reporter,
1212
show_base=show_base)
1214
def pull(self, source, overwrite=False, stop_revision=None,
1215
change_reporter=None, possible_transports=None, local=False,
1216
show_base=False, tag_selector=None):
1217
with self.lock_write(), source.lock_read():
1218
old_revision = self.branch.last_revision()
1219
count = self.branch.pull(source, overwrite, stop_revision,
1220
possible_transports=possible_transports,
1221
local=local, tag_selector=tag_selector)
1222
self._update_git_tree(
1223
old_revision=old_revision,
1224
new_revision=self.branch.last_revision(),
1225
change_reporter=change_reporter,
1226
show_base=show_base)
1229
def add_reference(self, sub_tree):
1230
"""Add a TreeReference to the tree, pointing at sub_tree.
1232
:param sub_tree: subtree to add.
1234
with self.lock_tree_write():
1236
sub_tree_path = self.relpath(sub_tree.basedir)
1237
except errors.PathNotChild:
1238
raise BadReferenceTarget(
1239
self, sub_tree, 'Target not inside tree.')
1241
self._add([sub_tree_path], [None], ['tree-reference'])
1243
def _read_submodule_head(self, path):
1244
return read_submodule_head(self.abspath(path))
1246
def get_reference_revision(self, path, branch=None):
1247
hexsha = self._read_submodule_head(path)
1249
return _mod_revision.NULL_REVISION
1250
return self.branch.lookup_foreign_revision_id(hexsha)
1252
def get_nested_tree(self, path):
1253
return workingtree.WorkingTree.open(self.abspath(path))
1255
def _directory_is_tree_reference(self, relpath):
1256
# as a special case, if a directory contains control files then
1257
# it's a tree reference, except that the root of the tree is not
1258
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1260
def extract(self, sub_path, format=None):
1261
"""Extract a subtree from this tree.
1263
A new branch will be created, relative to the path for this tree.
1266
segments = osutils.splitpath(path)
1267
transport = self.branch.controldir.root_transport
1268
for name in segments:
1269
transport = transport.clone(name)
1270
transport.ensure_base()
1273
with self.lock_tree_write():
1275
branch_transport = mkdirs(sub_path)
1277
format = self.controldir.cloning_metadir()
1278
branch_transport.ensure_base()
1279
branch_bzrdir = format.initialize_on_transport(branch_transport)
1281
repo = branch_bzrdir.find_repository()
1282
except errors.NoRepositoryPresent:
1283
repo = branch_bzrdir.create_repository()
1284
if not repo.supports_rich_root():
1285
raise errors.RootNotRich()
1286
new_branch = branch_bzrdir.create_branch()
1287
new_branch.pull(self.branch)
1288
for parent_id in self.get_parent_ids():
1289
new_branch.fetch(self.branch, parent_id)
1290
tree_transport = self.controldir.root_transport.clone(sub_path)
1291
if tree_transport.base != branch_transport.base:
1292
tree_bzrdir = format.initialize_on_transport(tree_transport)
1293
tree_bzrdir.set_branch_reference(new_branch)
1295
tree_bzrdir = branch_bzrdir
1296
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1297
wt.set_parent_ids(self.get_parent_ids())
1300
def _get_check_refs(self):
1301
"""Return the references needed to perform a check of this tree.
1303
The default implementation returns no refs, and is only suitable for
1304
trees that have no local caching and can commit on ghosts at any time.
1306
:seealso: breezy.check for details about check_refs.
1310
def copy_content_into(self, tree, revision_id=None):
1311
"""Copy the current content and user files of this tree into tree."""
1312
with self.lock_read():
1313
if revision_id is None:
1314
merge.transform_tree(tree, self)
1316
# TODO now merge from tree.last_revision to revision (to
1317
# preserve user local changes)
1319
other_tree = self.revision_tree(revision_id)
1320
except errors.NoSuchRevision:
1321
other_tree = self.branch.repository.revision_tree(
1324
merge.transform_tree(tree, other_tree)
1325
if revision_id == _mod_revision.NULL_REVISION:
1328
new_parents = [revision_id]
1329
tree.set_parent_ids(new_parents)
1331
def reference_parent(self, path, possible_transports=None):
1332
remote_url = self.get_reference_info(path)
1333
if remote_url is None:
1334
trace.warning("Unable to find submodule info for %s", path)
1336
return _mod_branch.Branch.open(remote_url, possible_transports=possible_transports)
1338
def get_reference_info(self, path):
1339
submodule_info = self._submodule_info()
1340
info = submodule_info.get(path.encode('utf-8'))
1343
return info[0].decode('utf-8')
1345
def set_reference_info(self, tree_path, branch_location):
1346
path = self.abspath('.gitmodules')
1348
config = GitConfigFile.from_path(path)
1349
except EnvironmentError as e:
1350
if e.errno == errno.ENOENT:
1351
config = GitConfigFile()
1354
section = (b'submodule', tree_path.encode('utf-8'))
1355
if branch_location is None:
1361
branch_location = urlutils.join(
1362
urlutils.strip_segment_parameters(self.branch.user_url),
1366
b'path', tree_path.encode('utf-8'))
1369
b'url', branch_location.encode('utf-8'))
1370
config.write_to_path(path)
1371
self.add('.gitmodules')
1374
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1376
_tree_class = GitWorkingTree
1378
supports_versioned_directories = False
1380
supports_setting_file_ids = False
1382
supports_store_uncommitted = False
1384
supports_leftmost_parent_id_as_ghost = False
1386
supports_righthand_parent_id_as_ghost = False
1388
requires_normalized_unicode_filenames = True
1390
supports_merge_modified = False
1392
ignore_filename = ".gitignore"
1395
def _matchingcontroldir(self):
1396
from .dir import LocalGitControlDirFormat
1397
return LocalGitControlDirFormat()
1399
def get_format_description(self):
1400
return "Git Working Tree"
1402
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1403
accelerator_tree=None, hardlink=False):
1404
"""See WorkingTreeFormat.initialize()."""
1405
if not isinstance(a_controldir, LocalGitDir):
1406
raise errors.IncompatibleFormat(self, a_controldir)
1407
branch = a_controldir.open_branch(nascent_ok=True)
1408
if revision_id is not None:
1409
branch.set_last_revision(revision_id)
1410
wt = GitWorkingTree(
1411
a_controldir, a_controldir.open_repository(), branch)
1412
for hook in MutableTree.hooks['post_build_tree']: