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,
60
revision as _mod_revision,
62
transport as _mod_transport,
67
from ..decorators import (
70
from ..mutabletree import (
74
from ..sixish import text_type
83
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, decode_git_path(relpath), '.git', 'index')
129
index_path = self.control_transport.local_abspath(
130
posixpath.join('modules', decode_git_path(info[1]), '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 merge_modified(self):
230
def set_merge_modified(self, modified_hashes):
231
raise errors.UnsupportedOperation(self.set_merge_modified, self)
233
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
234
self.set_parent_ids([p for p, t in parents_list])
236
def _set_merges_from_parent_ids(self, rhs_parent_ids):
238
merges = [self.branch.lookup_bzr_revision_id(
239
revid)[0] for revid in rhs_parent_ids]
240
except errors.NoSuchRevision as e:
241
raise errors.GhostRevisionUnusableHere(e.revision)
243
self.control_transport.put_bytes(
244
'MERGE_HEAD', b'\n'.join(merges),
245
mode=self.controldir._get_file_mode())
248
self.control_transport.delete('MERGE_HEAD')
249
except errors.NoSuchFile:
252
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
253
"""Set the parent ids to revision_ids.
255
See also set_parent_trees. This api will try to retrieve the tree data
256
for each element of revision_ids from the trees repository. If you have
257
tree data already available, it is more efficient to use
258
set_parent_trees rather than set_parent_ids. set_parent_ids is however
259
an easier API to use.
261
:param revision_ids: The revision_ids to set as the parent ids of this
262
working tree. Any of these may be ghosts.
264
with self.lock_tree_write():
265
self._check_parents_for_ghosts(
266
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
267
for revision_id in revision_ids:
268
_mod_revision.check_not_reserved_id(revision_id)
270
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
272
if len(revision_ids) > 0:
273
self.set_last_revision(revision_ids[0])
275
self.set_last_revision(_mod_revision.NULL_REVISION)
277
self._set_merges_from_parent_ids(revision_ids[1:])
279
def get_parent_ids(self):
280
"""See Tree.get_parent_ids.
282
This implementation reads the pending merges list and last_revision
283
value and uses that to decide what the parents list should be.
285
last_rev = _mod_revision.ensure_null(self._last_revision())
286
if _mod_revision.NULL_REVISION == last_rev:
291
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
292
except errors.NoSuchFile:
295
for l in osutils.split_lines(merges_bytes):
296
revision_id = l.rstrip(b'\n')
298
self.branch.lookup_foreign_revision_id(revision_id))
301
def check_state(self):
302
"""Check that the working state is/isn't valid."""
305
def remove(self, files, verbose=False, to_file=None, keep_files=True,
307
"""Remove nominated files from the working tree metadata.
309
:param files: File paths relative to the basedir.
310
:param keep_files: If true, the files will also be kept.
311
:param force: Delete files and directories, even if they are changed
312
and even if the directories are not empty.
314
if not isinstance(files, list):
320
def backup(file_to_backup):
321
abs_path = self.abspath(file_to_backup)
322
backup_name = self.controldir._available_backup_name(
324
osutils.rename(abs_path, self.abspath(backup_name))
325
return "removed %s (but kept a copy: %s)" % (
326
file_to_backup, backup_name)
328
# Sort needed to first handle directory content before the directory
333
def recurse_directory_to_add_files(directory):
334
# Recurse directory and add all files
335
# so we can check if they have changed.
336
for parent_info, file_infos in self.walkdirs(directory):
337
for relpath, basename, kind, lstat, fileid, kind in file_infos:
338
# Is it versioned or ignored?
339
if self.is_versioned(relpath):
340
# Add nested content for deletion.
341
all_files.add(relpath)
343
# Files which are not versioned
344
# should be treated as unknown.
345
files_to_backup.append(relpath)
347
with self.lock_tree_write():
348
for filepath in files:
349
# Get file name into canonical form.
350
abspath = self.abspath(filepath)
351
filepath = self.relpath(abspath)
354
all_files.add(filepath)
355
recurse_directory_to_add_files(filepath)
357
files = list(all_files)
360
return # nothing to do
362
# Sort needed to first handle directory content before the
364
files.sort(reverse=True)
366
# Bail out if we are going to delete files we shouldn't
367
if not keep_files and not force:
368
for change in self.iter_changes(
369
self.basis_tree(), include_unchanged=True,
370
require_versioned=False, want_unversioned=True,
371
specific_files=files):
372
if change.versioned[0] is False:
373
# The record is unknown or newly added
374
files_to_backup.append(change.path[1])
375
files_to_backup.extend(
376
osutils.parent_directories(change.path[1]))
377
elif (change.changed_content and (change.kind[1] is not None)
378
and osutils.is_inside_any(files, change.path[1])):
379
# Versioned and changed, but not deleted, and still
380
# in one of the dirs to be deleted.
381
files_to_backup.append(change.path[1])
382
files_to_backup.extend(
383
osutils.parent_directories(change.path[1]))
391
except errors.NoSuchFile:
394
abs_path = self.abspath(f)
396
# having removed it, it must be either ignored or unknown
397
if self.is_ignored(f):
401
kind_ch = osutils.kind_marker(kind)
402
to_file.write(new_status + ' ' + f + kind_ch + '\n')
404
message = "%s does not exist" % (f, )
407
if f in files_to_backup and not force:
410
if kind == 'directory':
411
osutils.rmtree(abs_path)
413
osutils.delete_any(abs_path)
414
message = "deleted %s" % (f,)
416
message = "removed %s" % (f,)
417
self._unversion_path(f)
419
# print only one message (if any) per file.
420
if message is not None:
422
self._versioned_dirs = None
424
def smart_add(self, file_list, recurse=True, action=None, save=True):
428
# expand any symlinks in the directory part, while leaving the
430
# only expanding if symlinks are supported avoids windows path bugs
431
if self.supports_symlinks():
432
file_list = list(map(osutils.normalizepath, file_list))
434
conflicts_related = set()
435
for c in self.conflicts():
436
conflicts_related.update(c.associated_filenames())
442
def call_action(filepath, kind):
445
if action is not None:
446
parent_path = posixpath.dirname(filepath)
447
parent_id = self.path2id(parent_path)
448
parent_ie = self._get_dir_ie(parent_path, parent_id)
449
file_id = action(self, parent_ie, filepath, kind)
450
if file_id is not None:
451
raise workingtree.SettingFileIdUnsupported()
453
with self.lock_tree_write():
454
for filepath in osutils.canonical_relpaths(
455
self.basedir, file_list):
456
filepath, can_access = osutils.normalized_filename(filepath)
458
raise errors.InvalidNormalization(filepath)
460
abspath = self.abspath(filepath)
461
kind = osutils.file_kind(abspath)
462
if kind in ("file", "symlink"):
463
(index, subpath) = self._lookup_index(
464
encode_git_path(filepath))
468
call_action(filepath, kind)
470
self._index_add_entry(filepath, kind)
471
added.append(filepath)
472
elif kind == "directory":
473
(index, subpath) = self._lookup_index(
474
encode_git_path(filepath))
475
if subpath not in index:
476
call_action(filepath, kind)
478
user_dirs.append(filepath)
480
raise errors.BadFileKindError(filename=abspath, kind=kind)
481
for user_dir in user_dirs:
482
abs_user_dir = self.abspath(user_dir)
485
transport = _mod_transport.get_transport_from_path(
487
_mod_controldir.ControlDirFormat.find_format(transport)
489
except errors.NotBranchError:
491
except errors.UnsupportedFormatError:
496
trace.warning('skipping nested tree %r', abs_user_dir)
499
for name in os.listdir(abs_user_dir):
500
subp = os.path.join(user_dir, name)
501
if (self.is_control_filename(subp) or
502
self.mapping.is_special_file(subp)):
504
ignore_glob = self.is_ignored(subp)
505
if ignore_glob is not None:
506
ignored.setdefault(ignore_glob, []).append(subp)
508
abspath = self.abspath(subp)
509
kind = osutils.file_kind(abspath)
510
if kind == "directory":
511
user_dirs.append(subp)
513
(index, subpath) = self._lookup_index(
514
encode_git_path(subp))
518
if subp in conflicts_related:
520
call_action(subp, kind)
522
self._index_add_entry(subp, kind)
524
return added, ignored
526
def has_filename(self, filename):
527
return osutils.lexists(self.abspath(filename))
529
def _iter_files_recursive(self, from_dir=None, include_dirs=False,
530
recurse_nested=False):
533
if not isinstance(from_dir, text_type):
534
raise TypeError(from_dir)
535
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
536
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
537
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
538
if self.controldir.is_control_filename(
539
dir_relpath.decode(osutils._fs_enc)):
541
for name in list(dirnames):
542
if self.controldir.is_control_filename(
543
name.decode(osutils._fs_enc)):
544
dirnames.remove(name)
546
relpath = os.path.join(dir_relpath, name)
547
if not recurse_nested and self._directory_is_tree_reference(relpath.decode(osutils._fs_enc)):
548
dirnames.remove(name)
551
yield relpath.decode(osutils._fs_enc)
552
except UnicodeDecodeError:
553
raise errors.BadFilenameEncoding(
554
relpath, osutils._fs_enc)
555
if not self.is_versioned(relpath.decode(osutils._fs_enc)):
556
dirnames.remove(name)
557
for name in filenames:
558
if self.mapping.is_special_file(name):
560
if self.controldir.is_control_filename(
561
name.decode(osutils._fs_enc, 'replace')):
563
yp = os.path.join(dir_relpath, name)
565
yield yp.decode(osutils._fs_enc)
566
except UnicodeDecodeError:
567
raise errors.BadFilenameEncoding(
571
"""Yield all unversioned files in this WorkingTree.
573
with self.lock_read():
575
[decode_git_path(p) for p, i in self._recurse_index_entries()])
576
all_paths = set(self._iter_files_recursive(include_dirs=False))
577
return iter(all_paths - index_paths)
579
def _gather_kinds(self, files, kinds):
580
"""See MutableTree._gather_kinds."""
581
with self.lock_tree_write():
582
for pos, f in enumerate(files):
583
if kinds[pos] is None:
584
fullpath = osutils.normpath(self.abspath(f))
586
kind = osutils.file_kind(fullpath)
588
if e.errno == errno.ENOENT:
589
raise errors.NoSuchFile(fullpath)
590
if f != '' and self._directory_is_tree_reference(f):
591
kind = 'tree-reference'
595
if self._lock_mode != 'w':
596
raise errors.NotWriteLocked(self)
597
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
598
# already in use and GitFile doesn't allow overriding the lock file
600
f = open(self.control_transport.local_abspath('index'), 'wb')
601
# Note that _flush will close the file
607
write_index_dict(shaf, self.index)
609
except BaseException:
612
self._index_dirty = False
614
def get_file_mtime(self, path):
615
"""See Tree.get_file_mtime."""
617
return self._lstat(path).st_mtime
619
if e.errno == errno.ENOENT:
620
raise errors.NoSuchFile(path)
623
def is_ignored(self, filename):
624
r"""Check whether the filename matches an ignore pattern.
626
If the file is ignored, returns the pattern which caused it to
627
be ignored, otherwise None. So this can simply be used as a
628
boolean if desired."""
629
if getattr(self, '_global_ignoreglobster', None) is None:
630
from breezy import ignores
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(encode_git_path(path))
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 = encode_git_path(path)
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(decode_git_path(path)).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(encode_git_path(path))
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 = encode_git_path(path)
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 = decode_git_path(path)
837
path = posixpath.dirname(path).strip("/")
843
def iter_child_entries(self, path):
844
encoded_path = encode_git_path(path)
845
with self.lock_read():
846
parent_id = self.path2id(path)
848
for item_path, value in self.index.iteritems():
849
decoded_item_path = decode_git_path(item_path)
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
decode_git_path(item_path)))
877
def set_conflicts(self, conflicts):
879
for conflict in conflicts:
880
if conflict.typestring in ('text conflict', 'contents conflict'):
881
by_path.add(encode_git_path(conflict.path))
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
encode_git_path(conflict.path), 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 = encode_git_path(prefix)
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 = decode_git_path(dirname)
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
(decode_git_path(path), decode_git_path(child_name),
1057
self.path2id(decode_git_path(path)),
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_transform_delta(self, changes):
1075
for (old_path, new_path, ie) in changes:
1076
if old_path is not None:
1077
(index, old_subpath) = self._lookup_index(
1078
encode_git_path(old_path))
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
decode_git_path(entry.path)))
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(
1202
self, old_revision, new_revision, change_reporter=None,
1204
basis_tree = self.revision_tree(old_revision)
1205
if new_revision != old_revision:
1206
from .. import merge
1207
with basis_tree.lock_read():
1208
new_basis_tree = self.branch.basis_tree()
1214
change_reporter=change_reporter,
1215
show_base=show_base)
1217
def pull(self, source, overwrite=False, stop_revision=None,
1218
change_reporter=None, possible_transports=None, local=False,
1219
show_base=False, tag_selector=None):
1220
with self.lock_write(), source.lock_read():
1221
old_revision = self.branch.last_revision()
1222
count = self.branch.pull(source, overwrite, stop_revision,
1223
possible_transports=possible_transports,
1224
local=local, tag_selector=tag_selector)
1225
self._update_git_tree(
1226
old_revision=old_revision,
1227
new_revision=self.branch.last_revision(),
1228
change_reporter=change_reporter,
1229
show_base=show_base)
1232
def add_reference(self, sub_tree):
1233
"""Add a TreeReference to the tree, pointing at sub_tree.
1235
:param sub_tree: subtree to add.
1237
with self.lock_tree_write():
1239
sub_tree_path = self.relpath(sub_tree.basedir)
1240
except errors.PathNotChild:
1241
raise BadReferenceTarget(
1242
self, sub_tree, 'Target not inside tree.')
1244
self._add([sub_tree_path], [None], ['tree-reference'])
1246
def _read_submodule_head(self, path):
1247
return read_submodule_head(self.abspath(path))
1249
def get_reference_revision(self, path, branch=None):
1250
hexsha = self._read_submodule_head(path)
1252
return _mod_revision.NULL_REVISION
1253
return self.branch.lookup_foreign_revision_id(hexsha)
1255
def get_nested_tree(self, path):
1256
return workingtree.WorkingTree.open(self.abspath(path))
1258
def _directory_is_tree_reference(self, relpath):
1259
# as a special case, if a directory contains control files then
1260
# it's a tree reference, except that the root of the tree is not
1261
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1263
def extract(self, sub_path, format=None):
1264
"""Extract a subtree from this tree.
1266
A new branch will be created, relative to the path for this tree.
1269
segments = osutils.splitpath(path)
1270
transport = self.branch.controldir.root_transport
1271
for name in segments:
1272
transport = transport.clone(name)
1273
transport.ensure_base()
1276
with self.lock_tree_write():
1278
branch_transport = mkdirs(sub_path)
1280
format = self.controldir.cloning_metadir()
1281
branch_transport.ensure_base()
1282
branch_bzrdir = format.initialize_on_transport(branch_transport)
1284
repo = branch_bzrdir.find_repository()
1285
except errors.NoRepositoryPresent:
1286
repo = branch_bzrdir.create_repository()
1287
if not repo.supports_rich_root():
1288
raise errors.RootNotRich()
1289
new_branch = branch_bzrdir.create_branch()
1290
new_branch.pull(self.branch)
1291
for parent_id in self.get_parent_ids():
1292
new_branch.fetch(self.branch, parent_id)
1293
tree_transport = self.controldir.root_transport.clone(sub_path)
1294
if tree_transport.base != branch_transport.base:
1295
tree_bzrdir = format.initialize_on_transport(tree_transport)
1296
tree_bzrdir.set_branch_reference(new_branch)
1298
tree_bzrdir = branch_bzrdir
1299
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1300
wt.set_parent_ids(self.get_parent_ids())
1303
def _get_check_refs(self):
1304
"""Return the references needed to perform a check of this tree.
1306
The default implementation returns no refs, and is only suitable for
1307
trees that have no local caching and can commit on ghosts at any time.
1309
:seealso: breezy.check for details about check_refs.
1313
def copy_content_into(self, tree, revision_id=None):
1314
"""Copy the current content and user files of this tree into tree."""
1315
from .. import merge
1316
with self.lock_read():
1317
if revision_id is None:
1318
merge.transform_tree(tree, self)
1320
# TODO now merge from tree.last_revision to revision (to
1321
# preserve user local changes)
1323
other_tree = self.revision_tree(revision_id)
1324
except errors.NoSuchRevision:
1325
other_tree = self.branch.repository.revision_tree(
1328
merge.transform_tree(tree, other_tree)
1329
if revision_id == _mod_revision.NULL_REVISION:
1332
new_parents = [revision_id]
1333
tree.set_parent_ids(new_parents)
1335
def reference_parent(self, path, possible_transports=None):
1336
remote_url = self.get_reference_info(path)
1337
if remote_url is None:
1338
trace.warning("Unable to find submodule info for %s", path)
1340
return _mod_branch.Branch.open(remote_url, possible_transports=possible_transports)
1342
def get_reference_info(self, path):
1343
submodule_info = self._submodule_info()
1344
info = submodule_info.get(encode_git_path(path))
1347
return decode_git_path(info[0])
1349
def set_reference_info(self, tree_path, branch_location):
1350
path = self.abspath('.gitmodules')
1352
config = GitConfigFile.from_path(path)
1353
except EnvironmentError as e:
1354
if e.errno == errno.ENOENT:
1355
config = GitConfigFile()
1358
section = (b'submodule', encode_git_path(tree_path))
1359
if branch_location is None:
1365
branch_location = urlutils.join(
1366
urlutils.strip_segment_parameters(self.branch.user_url),
1370
b'path', encode_git_path(tree_path))
1373
b'url', branch_location.encode('utf-8'))
1374
config.write_to_path(path)
1375
self.add('.gitmodules')
1378
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1380
_tree_class = GitWorkingTree
1382
supports_versioned_directories = False
1384
supports_setting_file_ids = False
1386
supports_store_uncommitted = False
1388
supports_leftmost_parent_id_as_ghost = False
1390
supports_righthand_parent_id_as_ghost = False
1392
requires_normalized_unicode_filenames = True
1394
supports_merge_modified = False
1396
ignore_filename = ".gitignore"
1399
def _matchingcontroldir(self):
1400
from .dir import LocalGitControlDirFormat
1401
return LocalGitControlDirFormat()
1403
def get_format_description(self):
1404
return "Git Working Tree"
1406
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1407
accelerator_tree=None, hardlink=False):
1408
"""See WorkingTreeFormat.initialize()."""
1409
if not isinstance(a_controldir, LocalGitDir):
1410
raise errors.IncompatibleFormat(self, a_controldir)
1411
branch = a_controldir.open_branch(nascent_ok=True)
1412
if revision_id is not None:
1413
branch.set_last_revision(revision_id)
1414
wt = GitWorkingTree(
1415
a_controldir, a_controldir.open_repository(), branch)
1416
for hook in MutableTree.hooks['post_build_tree']: