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,
58
revision as _mod_revision,
60
transport as _mod_transport,
65
from ..decorators import (
68
from ..mutabletree import (
80
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, decode_git_path(relpath), '.git', 'index')
126
index_path = self.control_transport.local_abspath(
127
posixpath.join('modules', decode_git_path(info[1]), 'index'))
128
return Index(index_path)
131
"""Lock the repository for read operations.
133
:return: A breezy.lock.LogicalLockResult.
135
if not self._lock_mode:
136
self._lock_mode = 'r'
140
self._lock_count += 1
141
self.branch.lock_read()
142
return lock.LogicalLockResult(self.unlock)
144
def _lock_write_tree(self):
145
if not self._lock_mode:
146
self._lock_mode = 'w'
149
self._index_file = GitFile(
150
self.control_transport.local_abspath('index'), 'wb')
152
raise errors.LockContention('index')
154
elif self._lock_mode == 'r':
155
raise errors.ReadOnlyError(self)
157
self._lock_count += 1
159
def lock_tree_write(self):
160
self.branch.lock_read()
162
self._lock_write_tree()
163
return lock.LogicalLockResult(self.unlock)
164
except BaseException:
168
def lock_write(self, token=None):
169
self.branch.lock_write()
171
self._lock_write_tree()
172
return lock.LogicalLockResult(self.unlock)
173
except BaseException:
178
return self._lock_count >= 1
180
def get_physical_lock_status(self):
183
def break_lock(self):
185
self.control_transport.delete('index.lock')
186
except errors.NoSuchFile:
188
self.branch.break_lock()
190
@only_raises(errors.LockNotHeld, errors.LockBroken)
192
if not self._lock_count:
193
return lock.cant_unlock_not_held(self)
196
self._lock_count -= 1
197
if self._lock_count > 0:
199
if self._index_file is not None:
200
if self._index_dirty:
201
self._flush(self._index_file)
202
self._index_file.close()
204
# Something else already triggered a write of the index
205
# file by calling .flush()
206
self._index_file.abort()
207
self._index_file = None
208
self._lock_mode = None
216
def _detect_case_handling(self):
218
self._transport.stat(".git/cOnFiG")
219
except errors.NoSuchFile:
220
self.case_sensitive = True
222
self.case_sensitive = False
224
def merge_modified(self):
227
def set_merge_modified(self, modified_hashes):
228
raise errors.UnsupportedOperation(self.set_merge_modified, self)
230
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
231
self.set_parent_ids([p for p, t in parents_list])
233
def _set_merges_from_parent_ids(self, rhs_parent_ids):
235
merges = [self.branch.lookup_bzr_revision_id(
236
revid)[0] for revid in rhs_parent_ids]
237
except errors.NoSuchRevision as e:
238
raise errors.GhostRevisionUnusableHere(e.revision)
240
self.control_transport.put_bytes(
241
'MERGE_HEAD', b'\n'.join(merges),
242
mode=self.controldir._get_file_mode())
245
self.control_transport.delete('MERGE_HEAD')
246
except errors.NoSuchFile:
249
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
250
"""Set the parent ids to revision_ids.
252
See also set_parent_trees. This api will try to retrieve the tree data
253
for each element of revision_ids from the trees repository. If you have
254
tree data already available, it is more efficient to use
255
set_parent_trees rather than set_parent_ids. set_parent_ids is however
256
an easier API to use.
258
:param revision_ids: The revision_ids to set as the parent ids of this
259
working tree. Any of these may be ghosts.
261
with self.lock_tree_write():
262
self._check_parents_for_ghosts(
263
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
264
for revision_id in revision_ids:
265
_mod_revision.check_not_reserved_id(revision_id)
267
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
269
if len(revision_ids) > 0:
270
self.set_last_revision(revision_ids[0])
272
self.set_last_revision(_mod_revision.NULL_REVISION)
274
self._set_merges_from_parent_ids(revision_ids[1:])
276
def get_parent_ids(self):
277
"""See Tree.get_parent_ids.
279
This implementation reads the pending merges list and last_revision
280
value and uses that to decide what the parents list should be.
282
last_rev = _mod_revision.ensure_null(self._last_revision())
283
if _mod_revision.NULL_REVISION == last_rev:
288
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
289
except errors.NoSuchFile:
292
for l in osutils.split_lines(merges_bytes):
293
revision_id = l.rstrip(b'\n')
295
self.branch.lookup_foreign_revision_id(revision_id))
298
def check_state(self):
299
"""Check that the working state is/isn't valid."""
302
def remove(self, files, verbose=False, to_file=None, keep_files=True,
304
"""Remove nominated files from the working tree metadata.
306
:param files: File paths relative to the basedir.
307
:param keep_files: If true, the files will also be kept.
308
:param force: Delete files and directories, even if they are changed
309
and even if the directories are not empty.
311
if not isinstance(files, list):
317
def backup(file_to_backup):
318
abs_path = self.abspath(file_to_backup)
319
backup_name = self.controldir._available_backup_name(
321
osutils.rename(abs_path, self.abspath(backup_name))
322
return "removed %s (but kept a copy: %s)" % (
323
file_to_backup, backup_name)
325
# Sort needed to first handle directory content before the directory
330
def recurse_directory_to_add_files(directory):
331
# Recurse directory and add all files
332
# so we can check if they have changed.
333
for parent_info, file_infos in self.walkdirs(directory):
334
for relpath, basename, kind, lstat, fileid, kind in file_infos:
335
# Is it versioned or ignored?
336
if self.is_versioned(relpath):
337
# Add nested content for deletion.
338
all_files.add(relpath)
340
# Files which are not versioned
341
# should be treated as unknown.
342
files_to_backup.append(relpath)
344
with self.lock_tree_write():
345
for filepath in files:
346
# Get file name into canonical form.
347
abspath = self.abspath(filepath)
348
filepath = self.relpath(abspath)
351
all_files.add(filepath)
352
recurse_directory_to_add_files(filepath)
354
files = list(all_files)
357
return # nothing to do
359
# Sort needed to first handle directory content before the
361
files.sort(reverse=True)
363
# Bail out if we are going to delete files we shouldn't
364
if not keep_files and not force:
365
for change in self.iter_changes(
366
self.basis_tree(), include_unchanged=True,
367
require_versioned=False, want_unversioned=True,
368
specific_files=files):
369
if change.versioned[0] is False:
370
# The record is unknown or newly added
371
files_to_backup.append(change.path[1])
372
files_to_backup.extend(
373
osutils.parent_directories(change.path[1]))
374
elif (change.changed_content and (change.kind[1] is not None)
375
and osutils.is_inside_any(files, change.path[1])):
376
# Versioned and changed, but not deleted, and still
377
# in one of the dirs to be deleted.
378
files_to_backup.append(change.path[1])
379
files_to_backup.extend(
380
osutils.parent_directories(change.path[1]))
388
except errors.NoSuchFile:
391
abs_path = self.abspath(f)
393
# having removed it, it must be either ignored or unknown
394
if self.is_ignored(f):
398
kind_ch = osutils.kind_marker(kind)
399
to_file.write(new_status + ' ' + f + kind_ch + '\n')
401
message = "%s does not exist" % (f, )
404
if f in files_to_backup and not force:
407
if kind == 'directory':
408
osutils.rmtree(abs_path)
410
osutils.delete_any(abs_path)
411
message = "deleted %s" % (f,)
413
message = "removed %s" % (f,)
414
self._unversion_path(f)
416
# print only one message (if any) per file.
417
if message is not None:
419
self._versioned_dirs = None
421
def smart_add(self, file_list, recurse=True, action=None, save=True):
425
# expand any symlinks in the directory part, while leaving the
427
# only expanding if symlinks are supported avoids windows path bugs
428
if self.supports_symlinks():
429
file_list = list(map(osutils.normalizepath, file_list))
431
conflicts_related = set()
432
for c in self.conflicts():
433
conflicts_related.update(c.associated_filenames())
439
def call_action(filepath, kind):
442
if action is not None:
443
parent_path = posixpath.dirname(filepath)
444
parent_id = self.path2id(parent_path)
445
parent_ie = self._get_dir_ie(parent_path, parent_id)
446
file_id = action(self, parent_ie, filepath, kind)
447
if file_id is not None:
448
raise workingtree.SettingFileIdUnsupported()
450
with self.lock_tree_write():
451
for filepath in osutils.canonical_relpaths(
452
self.basedir, file_list):
453
filepath, can_access = osutils.normalized_filename(filepath)
455
raise errors.InvalidNormalization(filepath)
457
abspath = self.abspath(filepath)
458
kind = osutils.file_kind(abspath)
459
if kind in ("file", "symlink"):
460
(index, subpath) = self._lookup_index(
461
encode_git_path(filepath))
465
call_action(filepath, kind)
467
self._index_add_entry(filepath, kind)
468
added.append(filepath)
469
elif kind == "directory":
470
(index, subpath) = self._lookup_index(
471
encode_git_path(filepath))
472
if subpath not in index:
473
call_action(filepath, kind)
475
user_dirs.append(filepath)
477
raise errors.BadFileKindError(filename=abspath, kind=kind)
478
for user_dir in user_dirs:
479
abs_user_dir = self.abspath(user_dir)
482
transport = _mod_transport.get_transport_from_path(
484
_mod_controldir.ControlDirFormat.find_format(transport)
486
except errors.NotBranchError:
488
except errors.UnsupportedFormatError:
493
trace.warning('skipping nested tree %r', abs_user_dir)
496
for name in os.listdir(abs_user_dir):
497
subp = os.path.join(user_dir, name)
498
if (self.is_control_filename(subp) or
499
self.mapping.is_special_file(subp)):
501
ignore_glob = self.is_ignored(subp)
502
if ignore_glob is not None:
503
ignored.setdefault(ignore_glob, []).append(subp)
505
abspath = self.abspath(subp)
506
kind = osutils.file_kind(abspath)
507
if kind == "directory":
508
user_dirs.append(subp)
510
(index, subpath) = self._lookup_index(
511
encode_git_path(subp))
515
if subp in conflicts_related:
517
call_action(subp, kind)
519
self._index_add_entry(subp, kind)
521
return added, ignored
523
def has_filename(self, filename):
524
return osutils.lexists(self.abspath(filename))
526
def _iter_files_recursive(self, from_dir=None, include_dirs=False,
527
recurse_nested=False):
530
if not isinstance(from_dir, str):
531
raise TypeError(from_dir)
532
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
533
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
534
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
535
if self.controldir.is_control_filename(
536
dir_relpath.decode(osutils._fs_enc)):
538
for name in list(dirnames):
539
if self.controldir.is_control_filename(
540
name.decode(osutils._fs_enc)):
541
dirnames.remove(name)
543
relpath = os.path.join(dir_relpath, name)
544
if not recurse_nested and self._directory_is_tree_reference(relpath.decode(osutils._fs_enc)):
545
dirnames.remove(name)
548
yield relpath.decode(osutils._fs_enc)
549
except UnicodeDecodeError:
550
raise errors.BadFilenameEncoding(
551
relpath, osutils._fs_enc)
552
if not self.is_versioned(relpath.decode(osutils._fs_enc)):
553
dirnames.remove(name)
554
for name in filenames:
555
if self.mapping.is_special_file(name):
557
if self.controldir.is_control_filename(
558
name.decode(osutils._fs_enc, 'replace')):
560
yp = os.path.join(dir_relpath, name)
562
yield yp.decode(osutils._fs_enc)
563
except UnicodeDecodeError:
564
raise errors.BadFilenameEncoding(
568
"""Yield all unversioned files in this WorkingTree.
570
with self.lock_read():
572
[decode_git_path(p) for p, i in self._recurse_index_entries()])
573
all_paths = set(self._iter_files_recursive(include_dirs=False))
574
return iter(all_paths - index_paths)
576
def _gather_kinds(self, files, kinds):
577
"""See MutableTree._gather_kinds."""
578
with self.lock_tree_write():
579
for pos, f in enumerate(files):
580
if kinds[pos] is None:
581
fullpath = osutils.normpath(self.abspath(f))
583
kind = osutils.file_kind(fullpath)
585
if e.errno == errno.ENOENT:
586
raise errors.NoSuchFile(fullpath)
587
if f != '' and self._directory_is_tree_reference(f):
588
kind = 'tree-reference'
592
if self._lock_mode != 'w':
593
raise errors.NotWriteLocked(self)
594
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
595
# already in use and GitFile doesn't allow overriding the lock file
597
f = open(self.control_transport.local_abspath('index'), 'wb')
598
# Note that _flush will close the file
604
write_index_dict(shaf, self.index)
606
except BaseException:
609
self._index_dirty = False
611
def get_file_mtime(self, path):
612
"""See Tree.get_file_mtime."""
614
return self._lstat(path).st_mtime
616
if e.errno == errno.ENOENT:
617
raise errors.NoSuchFile(path)
620
def is_ignored(self, filename):
621
r"""Check whether the filename matches an ignore pattern.
623
If the file is ignored, returns the pattern which caused it to
624
be ignored, otherwise None. So this can simply be used as a
625
boolean if desired."""
626
if getattr(self, '_global_ignoreglobster', None) is None:
627
from breezy import ignores
629
ignore_globs.update(ignores.get_runtime_ignores())
630
ignore_globs.update(ignores.get_user_ignores())
631
self._global_ignoreglobster = globbing.ExceptionGlobster(
633
match = self._global_ignoreglobster.match(filename)
634
if match is not None:
637
if self.kind(filename) == 'directory':
639
except errors.NoSuchFile:
641
filename = filename.lstrip('/')
642
ignore_manager = self._get_ignore_manager()
643
ps = list(ignore_manager.find_matching(filename))
646
if not ps[-1].is_exclude:
650
def _get_ignore_manager(self):
651
ignoremanager = getattr(self, '_ignoremanager', None)
652
if ignoremanager is not None:
655
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
656
self._ignoremanager = ignore_manager
657
return ignore_manager
659
def _flush_ignore_list_cache(self):
660
self._ignoremanager = None
662
def set_last_revision(self, revid):
663
if _mod_revision.is_null(revid):
664
self.branch.set_last_revision_info(0, revid)
666
_mod_revision.check_not_reserved_id(revid)
668
self.branch.generate_revision_history(revid)
669
except errors.NoSuchRevision:
670
raise errors.GhostRevisionUnusableHere(revid)
672
def _reset_data(self):
675
def get_file_verifier(self, path, stat_value=None):
676
with self.lock_read():
677
(index, subpath) = self._lookup_index(encode_git_path(path))
679
return ("GIT", index[subpath].sha)
681
if self._has_dir(path):
683
raise errors.NoSuchFile(path)
685
def get_file_sha1(self, path, stat_value=None):
686
with self.lock_read():
687
if not self.is_versioned(path):
688
raise errors.NoSuchFile(path)
689
abspath = self.abspath(path)
691
return osutils.sha_file_by_name(abspath)
693
if e.errno in (errno.EISDIR, errno.ENOENT):
697
def revision_tree(self, revid):
698
return self.repository.revision_tree(revid)
700
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
701
mode = stat_result.st_mode
702
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
704
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
705
return self.basis_tree().is_executable(path)
707
def stored_kind(self, path):
708
with self.lock_read():
709
encoded_path = encode_git_path(path)
710
(index, subpath) = self._lookup_index(encoded_path)
712
return mode_kind(index[subpath].mode)
714
# Maybe it's a directory?
715
if self._has_dir(encoded_path):
717
raise errors.NoSuchFile(path)
719
def _lstat(self, path):
720
return os.lstat(self.abspath(path))
722
def _live_entry(self, path):
723
encoded_path = self.abspath(decode_git_path(path)).encode(
725
return index_entry_from_path(encoded_path)
727
def is_executable(self, path):
728
with self.lock_read():
729
if self._supports_executable():
730
mode = self._lstat(path).st_mode
732
(index, subpath) = self._lookup_index(encode_git_path(path))
734
mode = index[subpath].mode
737
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
739
def _is_executable_from_path_and_stat(self, path, stat_result):
740
if self._supports_executable():
741
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
743
return self._is_executable_from_path_and_stat_from_basis(
746
def list_files(self, include_root=False, from_dir=None, recursive=True,
747
recurse_nested=False):
748
if from_dir is None or from_dir == '.':
751
fk_entries = {'directory': tree.TreeDirectory,
752
'file': tree.TreeFile,
753
'symlink': tree.TreeLink,
754
'tree-reference': tree.TreeReference}
755
with self.lock_read():
756
root_ie = self._get_dir_ie(u"", None)
757
if include_root and not from_dir:
758
yield "", "V", root_ie.kind, root_ie
759
dir_ids[u""] = root_ie.file_id
761
path_iterator = sorted(
762
self._iter_files_recursive(
763
from_dir, include_dirs=True,
764
recurse_nested=recurse_nested))
766
encoded_from_dir = self.abspath(from_dir).encode(
768
path_iterator = sorted(
769
[os.path.join(from_dir, name.decode(osutils._fs_enc))
770
for name in os.listdir(encoded_from_dir)
771
if not self.controldir.is_control_filename(
772
name.decode(osutils._fs_enc)) and
773
not self.mapping.is_special_file(
774
name.decode(osutils._fs_enc))])
775
for path in path_iterator:
777
encoded_path = encode_git_path(path)
778
except UnicodeEncodeError:
779
raise errors.BadFilenameEncoding(
780
path, osutils._fs_enc)
781
(index, index_path) = self._lookup_index(encoded_path)
783
value = index[index_path]
786
kind = self.kind(path)
787
parent, name = posixpath.split(path)
788
for dir_path, dir_ie in self._add_missing_parent_ids(
791
if kind == 'tree-reference' and recurse_nested:
792
ie = self._get_dir_ie(path, self.path2id(path))
793
yield (posixpath.relpath(path, from_dir), 'V', 'directory',
796
if kind == 'directory':
798
if self._has_dir(encoded_path):
799
ie = self._get_dir_ie(path, self.path2id(path))
801
elif self.is_ignored(path):
803
ie = fk_entries[kind]()
806
ie = fk_entries[kind]()
807
yield (posixpath.relpath(path, from_dir), status, kind,
810
if value is not None:
811
ie = self._get_file_ie(name, path, value, dir_ids[parent])
812
yield (posixpath.relpath(path, from_dir), "V", ie.kind, ie)
815
ie = fk_entries[kind]()
819
yield (posixpath.relpath(path, from_dir),
820
("I" if self.is_ignored(path) else "?"), kind, ie)
822
def all_file_ids(self):
823
raise errors.UnsupportedOperation(self.all_file_ids, self)
825
def all_versioned_paths(self):
826
with self.lock_read():
828
for path in self.index:
829
if self.mapping.is_special_file(path):
831
path = decode_git_path(path)
834
path = posixpath.dirname(path).strip("/")
840
def iter_child_entries(self, path):
841
encoded_path = encode_git_path(path)
842
with self.lock_read():
843
parent_id = self.path2id(path)
845
for item_path, value in self.index.iteritems():
846
decoded_item_path = decode_git_path(item_path)
847
if self.mapping.is_special_file(item_path):
849
if not osutils.is_inside(path, decoded_item_path):
852
subpath = posixpath.relpath(decoded_item_path, path)
854
dirname = subpath.split('/', 1)[0]
855
file_ie = self._get_dir_ie(
856
posixpath.join(path, dirname), parent_id)
858
(unused_parent, name) = posixpath.split(decoded_item_path)
859
file_ie = self._get_file_ie(
860
name, decoded_item_path, value, parent_id)
862
if not found_any and path != u'':
863
raise errors.NoSuchFile(path)
866
with self.lock_read():
867
conflicts = _mod_conflicts.ConflictList()
868
for item_path, value in self.index.iteritems():
869
if value.flags & FLAG_STAGEMASK:
870
conflicts.append(_mod_conflicts.TextConflict(
871
decode_git_path(item_path)))
874
def set_conflicts(self, conflicts):
876
for conflict in conflicts:
877
if conflict.typestring in ('text conflict', 'contents conflict'):
878
by_path.add(encode_git_path(conflict.path))
880
raise errors.UnsupportedOperation(self.set_conflicts, self)
881
with self.lock_tree_write():
882
for path in self.index:
883
self._set_conflicted(path, path in by_path)
885
def _set_conflicted(self, path, conflicted):
886
trace.mutter('change conflict: %r -> %r', path, conflicted)
887
value = self.index[path]
888
self._index_dirty = True
890
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
892
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
894
def add_conflicts(self, new_conflicts):
895
with self.lock_tree_write():
896
for conflict in new_conflicts:
897
if conflict.typestring in ('text conflict',
898
'contents conflict'):
900
self._set_conflicted(
901
encode_git_path(conflict.path), True)
903
raise errors.UnsupportedOperation(
904
self.add_conflicts, self)
906
raise errors.UnsupportedOperation(self.add_conflicts, self)
908
def walkdirs(self, prefix=""):
909
"""Walk the directories of this tree.
911
returns a generator which yields items in the form:
912
((curren_directory_path, fileid),
913
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
916
This API returns a generator, which is only valid during the current
917
tree transaction - within a single lock_read or lock_write duration.
919
If the tree is not locked, it may cause an error to be raised,
920
depending on the tree implementation.
922
from bisect import bisect_left
924
disk_top = self.abspath(prefix)
925
if disk_top.endswith('/'):
926
disk_top = disk_top[:-1]
927
top_strip_len = len(disk_top) + 1
928
inventory_iterator = self._walkdirs(prefix)
929
disk_iterator = osutils.walkdirs(disk_top, prefix)
931
current_disk = next(disk_iterator)
932
disk_finished = False
934
if not (e.errno == errno.ENOENT
935
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
940
current_inv = next(inventory_iterator)
942
except StopIteration:
945
while not inv_finished or not disk_finished:
947
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
948
cur_disk_dir_content) = current_disk
950
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
951
cur_disk_dir_content) = ((None, None), None)
952
if not disk_finished:
953
# strip out .bzr dirs
954
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
955
and len(cur_disk_dir_content) > 0):
956
# osutils.walkdirs can be made nicer -
957
# yield the path-from-prefix rather than the pathjoined
959
bzrdir_loc = bisect_left(cur_disk_dir_content,
961
if (bzrdir_loc < len(cur_disk_dir_content) and
962
self.controldir.is_control_filename(
963
cur_disk_dir_content[bzrdir_loc][0])):
964
# we dont yield the contents of, or, .bzr itself.
965
del cur_disk_dir_content[bzrdir_loc]
967
# everything is unknown
970
# everything is missing
973
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
974
- (current_inv[0][0] < cur_disk_dir_relpath))
976
# disk is before inventory - unknown
977
dirblock = [(relpath, basename, kind, stat, None, None) for
978
relpath, basename, kind, stat, top_path in
979
cur_disk_dir_content]
980
yield (cur_disk_dir_relpath, None), dirblock
982
current_disk = next(disk_iterator)
983
except StopIteration:
986
# inventory is before disk - missing.
987
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
988
for relpath, basename, dkind, stat, fileid, kind in
990
yield (current_inv[0][0], current_inv[0][1]), dirblock
992
current_inv = next(inventory_iterator)
993
except StopIteration:
996
# versioned present directory
997
# merge the inventory and disk data together
999
for relpath, subiterator in itertools.groupby(sorted(
1000
current_inv[1] + cur_disk_dir_content,
1001
key=operator.itemgetter(0)), operator.itemgetter(1)):
1002
path_elements = list(subiterator)
1003
if len(path_elements) == 2:
1004
inv_row, disk_row = path_elements
1005
# versioned, present file
1006
dirblock.append((inv_row[0],
1007
inv_row[1], disk_row[2],
1008
disk_row[3], inv_row[4],
1010
elif len(path_elements[0]) == 5:
1013
(path_elements[0][0], path_elements[0][1],
1014
path_elements[0][2], path_elements[0][3],
1016
elif len(path_elements[0]) == 6:
1017
# versioned, absent file.
1019
(path_elements[0][0], path_elements[0][1],
1020
'unknown', None, path_elements[0][4],
1021
path_elements[0][5]))
1023
raise NotImplementedError('unreachable code')
1024
yield current_inv[0], dirblock
1026
current_inv = next(inventory_iterator)
1027
except StopIteration:
1030
current_disk = next(disk_iterator)
1031
except StopIteration:
1032
disk_finished = True
1034
def _walkdirs(self, prefix=u""):
1037
prefix = encode_git_path(prefix)
1038
per_dir = defaultdict(set)
1040
per_dir[(u'', self.path2id(''))] = set()
1042
def add_entry(path, kind):
1043
if path == b'' or not path.startswith(prefix):
1045
(dirname, child_name) = posixpath.split(path)
1046
add_entry(dirname, 'directory')
1047
dirname = decode_git_path(dirname)
1048
dir_file_id = self.path2id(dirname)
1049
if not isinstance(value, tuple) or len(value) != 10:
1050
raise ValueError(value)
1051
per_dir[(dirname, dir_file_id)].add(
1052
(decode_git_path(path), decode_git_path(child_name),
1054
self.path2id(decode_git_path(path)),
1056
with self.lock_read():
1057
for path, value in self.index.iteritems():
1058
if self.mapping.is_special_file(path):
1060
if not path.startswith(prefix):
1062
add_entry(path, mode_kind(value.mode))
1063
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1065
def get_shelf_manager(self):
1066
raise workingtree.ShelvingUnsupported()
1068
def store_uncommitted(self):
1069
raise errors.StoringUncommittedNotSupported(self)
1071
def _apply_transform_delta(self, changes):
1072
for (old_path, new_path, ie) in changes:
1073
if old_path is not None:
1074
(index, old_subpath) = self._lookup_index(
1075
encode_git_path(old_path))
1077
self._index_del_entry(index, old_subpath)
1081
self._versioned_dirs = None
1082
if new_path is not None and ie.kind != 'directory':
1083
if ie.kind == 'tree-reference':
1084
self._index_add_entry(
1086
reference_revision=ie.reference_revision)
1088
self._index_add_entry(new_path, ie.kind)
1091
def annotate_iter(self, path,
1092
default_revision=_mod_revision.CURRENT_REVISION):
1093
"""See Tree.annotate_iter
1095
This implementation will use the basis tree implementation if possible.
1096
Lines not in the basis are attributed to CURRENT_REVISION
1098
If there are pending merges, lines added by those merges will be
1099
incorrectly attributed to CURRENT_REVISION (but after committing, the
1100
attribution will be correct).
1102
with self.lock_read():
1103
maybe_file_parent_keys = []
1104
for parent_id in self.get_parent_ids():
1106
parent_tree = self.revision_tree(parent_id)
1107
except errors.NoSuchRevisionInTree:
1108
parent_tree = self.branch.repository.revision_tree(
1110
with parent_tree.lock_read():
1111
# TODO(jelmer): Use rename/copy tracker to find path name
1115
kind = parent_tree.kind(parent_path)
1116
except errors.NoSuchFile:
1119
# Note: this is slightly unnecessary, because symlinks
1120
# and directories have a "text" which is the empty
1121
# text, and we know that won't mess up annotations. But
1126
parent_tree.get_file_revision(parent_path))
1127
if parent_text_key not in maybe_file_parent_keys:
1128
maybe_file_parent_keys.append(parent_text_key)
1129
# Now we have the parents of this content
1130
from breezy.annotate import Annotator
1131
from .annotate import AnnotateProvider
1132
annotate_provider = AnnotateProvider(
1133
self.branch.repository._file_change_scanner)
1134
annotator = Annotator(annotate_provider)
1136
from breezy.graph import Graph
1137
graph = Graph(annotate_provider)
1138
heads = graph.heads(maybe_file_parent_keys)
1139
file_parent_keys = []
1140
for key in maybe_file_parent_keys:
1142
file_parent_keys.append(key)
1144
text = self.get_file_text(path)
1145
this_key = (path, default_revision)
1146
annotator.add_special_text(this_key, file_parent_keys, text)
1147
annotations = [(key[-1], line)
1148
for key, line in annotator.annotate_flat(this_key)]
1151
def _rename_one(self, from_rel, to_rel):
1152
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1154
def _build_checkout_with_index(self):
1155
build_index_from_tree(
1156
self.user_transport.local_abspath('.'),
1157
self.control_transport.local_abspath("index"),
1160
if self.branch.head is None
1161
else self.store[self.branch.head].tree,
1162
honor_filemode=self._supports_executable())
1164
def reset_state(self, revision_ids=None):
1165
"""Reset the state of the working tree.
1167
This does a hard-reset to a last-known-good state. This is a way to
1168
fix if something got corrupted (like the .git/index file)
1170
with self.lock_tree_write():
1171
if revision_ids is not None:
1172
self.set_parent_ids(revision_ids)
1174
self._index_dirty = True
1175
if self.branch.head is not None:
1176
for entry in self.store.iter_tree_contents(
1177
self.store[self.branch.head].tree):
1178
if not validate_path(entry.path):
1181
if S_ISGITLINK(entry.mode):
1182
pass # TODO(jelmer): record and return submodule paths
1184
# Let's at least try to use the working tree file:
1186
st = self._lstat(self.abspath(
1187
decode_git_path(entry.path)))
1189
# But if it doesn't exist, we'll make something up.
1190
obj = self.store[entry.sha]
1191
st = os.stat_result((entry.mode, 0, 0, 0,
1193
obj.as_raw_string()), 0,
1195
(index, subpath) = self._lookup_index(entry.path)
1196
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1198
def _update_git_tree(
1199
self, old_revision, new_revision, change_reporter=None,
1201
basis_tree = self.revision_tree(old_revision)
1202
if new_revision != old_revision:
1203
from .. import merge
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
from .. import merge
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(encode_git_path(path))
1344
return decode_git_path(info[0])
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', encode_git_path(tree_path))
1356
if branch_location is None:
1362
branch_location = urlutils.join(
1363
urlutils.strip_segment_parameters(self.branch.user_url),
1367
b'path', encode_git_path(tree_path))
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']: