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.file import GitFile, FileLocked
29
from dulwich.index import (
32
build_index_from_tree,
33
index_entry_from_path,
34
index_entry_from_stat,
40
from dulwich.object_store import (
43
from dulwich.objects import (
52
conflicts as _mod_conflicts,
54
controldir as _mod_controldir,
60
revision as _mod_revision,
62
transport as _mod_transport,
66
from ..decorators import (
69
from ..mutabletree import (
81
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
119
"""Lock the repository for read operations.
121
:return: A breezy.lock.LogicalLockResult.
123
if not self._lock_mode:
124
self._lock_mode = 'r'
128
self._lock_count += 1
129
self.branch.lock_read()
130
return lock.LogicalLockResult(self.unlock)
132
def _lock_write_tree(self):
133
if not self._lock_mode:
134
self._lock_mode = 'w'
137
self._index_file = GitFile(
138
self.control_transport.local_abspath('index'), 'wb')
140
raise errors.LockContention('index')
142
elif self._lock_mode == 'r':
143
raise errors.ReadOnlyError(self)
145
self._lock_count += 1
147
def lock_tree_write(self):
148
self.branch.lock_read()
150
self._lock_write_tree()
151
return lock.LogicalLockResult(self.unlock)
152
except BaseException:
156
def lock_write(self, token=None):
157
self.branch.lock_write()
159
self._lock_write_tree()
160
return lock.LogicalLockResult(self.unlock)
161
except BaseException:
166
return self._lock_count >= 1
168
def get_physical_lock_status(self):
171
def break_lock(self):
173
self.control_transport.delete('index.lock')
174
except errors.NoSuchFile:
176
self.branch.break_lock()
178
@only_raises(errors.LockNotHeld, errors.LockBroken)
180
if not self._lock_count:
181
return lock.cant_unlock_not_held(self)
184
self._lock_count -= 1
185
if self._lock_count > 0:
187
if self._index_file is not None:
188
if self._index_dirty:
189
self._flush(self._index_file)
190
self._index_file.close()
192
# Something else already triggered a write of the index
193
# file by calling .flush()
194
self._index_file.abort()
195
self._index_file = None
196
self._lock_mode = None
204
def _detect_case_handling(self):
206
self._transport.stat(".git/cOnFiG")
207
except errors.NoSuchFile:
208
self.case_sensitive = True
210
self.case_sensitive = False
212
def merge_modified(self):
215
def set_merge_modified(self, modified_hashes):
216
raise errors.UnsupportedOperation(self.set_merge_modified, self)
218
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
219
self.set_parent_ids([p for p, t in parents_list])
221
def _set_merges_from_parent_ids(self, rhs_parent_ids):
223
merges = [self.branch.lookup_bzr_revision_id(
224
revid)[0] for revid in rhs_parent_ids]
225
except errors.NoSuchRevision as e:
226
raise errors.GhostRevisionUnusableHere(e.revision)
228
self.control_transport.put_bytes(
229
'MERGE_HEAD', b'\n'.join(merges),
230
mode=self.controldir._get_file_mode())
233
self.control_transport.delete('MERGE_HEAD')
234
except errors.NoSuchFile:
237
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
238
"""Set the parent ids to revision_ids.
240
See also set_parent_trees. This api will try to retrieve the tree data
241
for each element of revision_ids from the trees repository. If you have
242
tree data already available, it is more efficient to use
243
set_parent_trees rather than set_parent_ids. set_parent_ids is however
244
an easier API to use.
246
:param revision_ids: The revision_ids to set as the parent ids of this
247
working tree. Any of these may be ghosts.
249
with self.lock_tree_write():
250
self._check_parents_for_ghosts(
251
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
252
for revision_id in revision_ids:
253
_mod_revision.check_not_reserved_id(revision_id)
255
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
257
if len(revision_ids) > 0:
258
self.set_last_revision(revision_ids[0])
260
self.set_last_revision(_mod_revision.NULL_REVISION)
262
self._set_merges_from_parent_ids(revision_ids[1:])
264
def get_parent_ids(self):
265
"""See Tree.get_parent_ids.
267
This implementation reads the pending merges list and last_revision
268
value and uses that to decide what the parents list should be.
270
last_rev = _mod_revision.ensure_null(self._last_revision())
271
if _mod_revision.NULL_REVISION == last_rev:
276
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
277
except errors.NoSuchFile:
280
for l in osutils.split_lines(merges_bytes):
281
revision_id = l.rstrip(b'\n')
283
self.branch.lookup_foreign_revision_id(revision_id))
286
def check_state(self):
287
"""Check that the working state is/isn't valid."""
290
def remove(self, files, verbose=False, to_file=None, keep_files=True,
292
"""Remove nominated files from the working tree metadata.
294
:param files: File paths relative to the basedir.
295
:param keep_files: If true, the files will also be kept.
296
:param force: Delete files and directories, even if they are changed
297
and even if the directories are not empty.
299
if not isinstance(files, list):
305
def backup(file_to_backup):
306
abs_path = self.abspath(file_to_backup)
307
backup_name = self.controldir._available_backup_name(
309
osutils.rename(abs_path, self.abspath(backup_name))
310
return "removed %s (but kept a copy: %s)" % (
311
file_to_backup, backup_name)
313
# Sort needed to first handle directory content before the directory
318
def recurse_directory_to_add_files(directory):
319
# Recurse directory and add all files
320
# so we can check if they have changed.
321
for parent_info, file_infos in self.walkdirs(directory):
322
for relpath, basename, kind, lstat, fileid, kind in file_infos:
323
# Is it versioned or ignored?
324
if self.is_versioned(relpath):
325
# Add nested content for deletion.
326
all_files.add(relpath)
328
# Files which are not versioned
329
# should be treated as unknown.
330
files_to_backup.append(relpath)
332
with self.lock_tree_write():
333
for filepath in files:
334
# Get file name into canonical form.
335
abspath = self.abspath(filepath)
336
filepath = self.relpath(abspath)
339
all_files.add(filepath)
340
recurse_directory_to_add_files(filepath)
342
files = list(all_files)
345
return # nothing to do
347
# Sort needed to first handle directory content before the
349
files.sort(reverse=True)
351
# Bail out if we are going to delete files we shouldn't
352
if not keep_files and not force:
353
for change in self.iter_changes(
354
self.basis_tree(), include_unchanged=True,
355
require_versioned=False, want_unversioned=True,
356
specific_files=files):
357
if change.versioned[0] is False:
358
# The record is unknown or newly added
359
files_to_backup.append(change.path[1])
360
files_to_backup.extend(
361
osutils.parent_directories(change.path[1]))
362
elif (change.changed_content and (change.kind[1] is not None)
363
and osutils.is_inside_any(files, change.path[1])):
364
# Versioned and changed, but not deleted, and still
365
# in one of the dirs to be deleted.
366
files_to_backup.append(change.path[1])
367
files_to_backup.extend(
368
osutils.parent_directories(change.path[1]))
376
except errors.NoSuchFile:
379
abs_path = self.abspath(f)
381
# having removed it, it must be either ignored or unknown
382
if self.is_ignored(f):
386
kind_ch = osutils.kind_marker(kind)
387
to_file.write(new_status + ' ' + f + kind_ch + '\n')
389
message = "%s does not exist" % (f, )
392
if f in files_to_backup and not force:
395
if kind == 'directory':
396
osutils.rmtree(abs_path)
398
osutils.delete_any(abs_path)
399
message = "deleted %s" % (f,)
401
message = "removed %s" % (f,)
402
self._unversion_path(f)
404
# print only one message (if any) per file.
405
if message is not None:
407
self._versioned_dirs = None
409
def smart_add(self, file_list, recurse=True, action=None, save=True):
413
# expand any symlinks in the directory part, while leaving the
415
# only expanding if symlinks are supported avoids windows path bugs
416
if self.supports_symlinks():
417
file_list = list(map(osutils.normalizepath, file_list))
419
conflicts_related = set()
420
for c in self.conflicts():
421
conflicts_related.update(c.associated_filenames())
427
def call_action(filepath, kind):
430
if action is not None:
431
parent_path = posixpath.dirname(filepath)
432
parent_id = self.path2id(parent_path)
433
parent_ie = self._get_dir_ie(parent_path, parent_id)
434
file_id = action(self, parent_ie, filepath, kind)
435
if file_id is not None:
436
raise workingtree.SettingFileIdUnsupported()
438
with self.lock_tree_write():
439
for filepath in osutils.canonical_relpaths(
440
self.basedir, file_list):
441
filepath, can_access = osutils.normalized_filename(filepath)
443
raise errors.InvalidNormalization(filepath)
445
abspath = self.abspath(filepath)
446
kind = osutils.file_kind(abspath)
447
if kind in ("file", "symlink"):
448
(index, subpath) = self._lookup_index(
449
filepath.encode('utf-8'))
453
call_action(filepath, kind)
455
self._index_add_entry(filepath, kind)
456
added.append(filepath)
457
elif kind == "directory":
458
(index, subpath) = self._lookup_index(
459
filepath.encode('utf-8'))
460
if subpath not in index:
461
call_action(filepath, kind)
463
user_dirs.append(filepath)
465
raise errors.BadFileKindError(filename=abspath, kind=kind)
466
for user_dir in user_dirs:
467
abs_user_dir = self.abspath(user_dir)
470
transport = _mod_transport.get_transport_from_path(
472
_mod_controldir.ControlDirFormat.find_format(transport)
474
except errors.NotBranchError:
476
except errors.UnsupportedFormatError:
481
trace.warning('skipping nested tree %r', abs_user_dir)
484
for name in os.listdir(abs_user_dir):
485
subp = os.path.join(user_dir, name)
486
if (self.is_control_filename(subp) or
487
self.mapping.is_special_file(subp)):
489
ignore_glob = self.is_ignored(subp)
490
if ignore_glob is not None:
491
ignored.setdefault(ignore_glob, []).append(subp)
493
abspath = self.abspath(subp)
494
kind = osutils.file_kind(abspath)
495
if kind == "directory":
496
user_dirs.append(subp)
498
(index, subpath) = self._lookup_index(
499
subp.encode('utf-8'))
503
if subp in conflicts_related:
505
call_action(subp, kind)
507
self._index_add_entry(subp, kind)
509
return added, ignored
511
def has_filename(self, filename):
512
return osutils.lexists(self.abspath(filename))
514
def _iter_files_recursive(self, from_dir=None, include_dirs=False):
517
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
518
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
519
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
520
if self.controldir.is_control_filename(
521
dir_relpath.decode(osutils._fs_enc)):
523
for name in list(dirnames):
524
if self.controldir.is_control_filename(
525
name.decode(osutils._fs_enc)):
526
dirnames.remove(name)
528
relpath = os.path.join(dir_relpath, name)
531
yield relpath.decode(osutils._fs_enc)
532
except UnicodeDecodeError:
533
raise errors.BadFilenameEncoding(
534
relpath, osutils._fs_enc)
535
if not self._has_dir(relpath):
536
dirnames.remove(name)
537
for name in filenames:
538
if self.mapping.is_special_file(name):
540
if self.controldir.is_control_filename(
541
name.decode(osutils._fs_enc, 'replace')):
543
yp = os.path.join(dir_relpath, name)
545
yield yp.decode(osutils._fs_enc)
546
except UnicodeDecodeError:
547
raise errors.BadFilenameEncoding(
551
"""Yield all unversioned files in this WorkingTree.
553
with self.lock_read():
555
[p.decode('utf-8') for p, i in self._recurse_index_entries()])
556
all_paths = set(self._iter_files_recursive(include_dirs=False))
557
return iter(all_paths - index_paths)
559
def _gather_kinds(self, files, kinds):
560
"""See MutableTree._gather_kinds."""
561
with self.lock_tree_write():
562
for pos, f in enumerate(files):
563
if kinds[pos] is None:
564
fullpath = osutils.normpath(self.abspath(f))
566
kind = osutils.file_kind(fullpath)
568
if e.errno == errno.ENOENT:
569
raise errors.NoSuchFile(fullpath)
570
if f != '' and self._directory_is_tree_reference(f):
571
kind = 'tree-reference'
575
if self._lock_mode != 'w':
576
raise errors.NotWriteLocked(self)
577
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
578
# already in use and GitFile doesn't allow overriding the lock file
580
f = open(self.control_transport.local_abspath('index'), 'wb')
581
# Note that _flush will close the file
587
write_index_dict(shaf, self.index)
589
except BaseException:
592
self._index_dirty = False
594
def has_or_had_id(self, file_id):
595
if self.has_id(file_id):
597
if self.had_id(file_id):
601
def had_id(self, file_id):
602
path = self._basis_fileid_map.lookup_path(file_id)
604
head = self.repository._git.head()
606
# Assume no if basis is not accessible
609
root_tree = self.store[head].tree
613
tree_lookup_path(self.store.__getitem__,
614
root_tree, path.encode('utf-8'))
620
def get_file_mtime(self, path):
621
"""See Tree.get_file_mtime."""
623
return self._lstat(path).st_mtime
625
if e.errno == errno.ENOENT:
626
raise errors.NoSuchFile(path)
629
def is_ignored(self, filename):
630
r"""Check whether the filename matches an ignore pattern.
632
If the file is ignored, returns the pattern which caused it to
633
be ignored, otherwise None. So this can simply be used as a
634
boolean if desired."""
635
if getattr(self, '_global_ignoreglobster', None) is None:
637
ignore_globs.update(ignores.get_runtime_ignores())
638
ignore_globs.update(ignores.get_user_ignores())
639
self._global_ignoreglobster = globbing.ExceptionGlobster(
641
match = self._global_ignoreglobster.match(filename)
642
if match is not None:
645
if self.kind(filename) == 'directory':
647
except errors.NoSuchFile:
649
filename = filename.lstrip('/')
650
ignore_manager = self._get_ignore_manager()
651
ps = list(ignore_manager.find_matching(filename))
654
if not ps[-1].is_exclude:
658
def _get_ignore_manager(self):
659
ignoremanager = getattr(self, '_ignoremanager', None)
660
if ignoremanager is not None:
663
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
664
self._ignoremanager = ignore_manager
665
return ignore_manager
667
def _flush_ignore_list_cache(self):
668
self._ignoremanager = None
670
def set_last_revision(self, revid):
671
if _mod_revision.is_null(revid):
672
self.branch.set_last_revision_info(0, revid)
674
_mod_revision.check_not_reserved_id(revid)
676
self.branch.generate_revision_history(revid)
677
except errors.NoSuchRevision:
678
raise errors.GhostRevisionUnusableHere(revid)
680
def _reset_data(self):
682
head = self.repository._git.head()
684
self._basis_fileid_map = GitFileIdMap({}, self.mapping)
686
self._basis_fileid_map = self.mapping.get_fileid_map(
687
self.store.__getitem__, self.store[head].tree)
688
self._fileid_map = self._basis_fileid_map.copy()
690
def get_file_verifier(self, path, stat_value=None):
691
with self.lock_read():
692
(index, subpath) = self._lookup_index(path.encode('utf-8'))
694
return ("GIT", index[subpath].sha)
696
if self._has_dir(path):
698
raise errors.NoSuchFile(path)
700
def get_file_sha1(self, path, stat_value=None):
701
with self.lock_read():
702
if not self.is_versioned(path):
703
raise errors.NoSuchFile(path)
704
abspath = self.abspath(path)
706
return osutils.sha_file_by_name(abspath)
708
if e.errno in (errno.EISDIR, errno.ENOENT):
712
def revision_tree(self, revid):
713
return self.repository.revision_tree(revid)
715
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
716
mode = stat_result.st_mode
717
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
719
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
720
return self.basis_tree().is_executable(path)
722
def stored_kind(self, path):
723
with self.lock_read():
724
encoded_path = path.encode('utf-8')
725
(index, subpath) = self._lookup_index(encoded_path)
727
return mode_kind(index[subpath].mode)
729
# Maybe it's a directory?
730
if self._has_dir(encoded_path):
732
raise errors.NoSuchFile(path)
734
def _lstat(self, path):
735
return os.lstat(self.abspath(path))
737
def _live_entry(self, path):
738
encoded_path = self.abspath(path.decode('utf-8')).encode(
740
return index_entry_from_path(encoded_path)
742
def is_executable(self, path):
743
with self.lock_read():
744
if self._supports_executable():
745
mode = self._lstat(path).st_mode
747
(index, subpath) = self._lookup_index(path.encode('utf-8'))
749
mode = index[subpath].mode
752
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
754
def _is_executable_from_path_and_stat(self, path, stat_result):
755
if self._supports_executable():
756
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
758
return self._is_executable_from_path_and_stat_from_basis(
761
def list_files(self, include_root=False, from_dir=None, recursive=True):
762
if from_dir is None or from_dir == '.':
765
fk_entries = {'directory': tree.TreeDirectory,
766
'file': tree.TreeFile,
767
'symlink': tree.TreeLink,
768
'tree-reference': tree.TreeReference}
769
with self.lock_read():
770
root_ie = self._get_dir_ie(u"", None)
771
if include_root and not from_dir:
772
yield "", "V", root_ie.kind, root_ie
773
dir_ids[u""] = root_ie.file_id
775
path_iterator = sorted(
776
self._iter_files_recursive(from_dir, include_dirs=True))
778
encoded_from_dir = self.abspath(from_dir).encode(
780
path_iterator = sorted(
781
[os.path.join(from_dir, name.decode(osutils._fs_enc))
782
for name in os.listdir(encoded_from_dir)
783
if not self.controldir.is_control_filename(
784
name.decode(osutils._fs_enc)) and
785
not self.mapping.is_special_file(
786
name.decode(osutils._fs_enc))])
787
for path in path_iterator:
789
encoded_path = path.encode("utf-8")
790
except UnicodeEncodeError:
791
raise errors.BadFilenameEncoding(
792
path, osutils._fs_enc)
793
(index, index_path) = self._lookup_index(encoded_path)
795
value = index[index_path]
798
kind = self.kind(path)
799
parent, name = posixpath.split(path)
800
for dir_path, dir_ie in self._add_missing_parent_ids(
803
if kind in ('directory', 'tree-reference'):
805
if self._has_dir(encoded_path):
806
ie = self._get_dir_ie(path, self.path2id(path))
808
elif self.is_ignored(path):
810
ie = fk_entries[kind]()
813
ie = fk_entries[kind]()
814
yield (posixpath.relpath(path, from_dir), status, kind,
817
if value is not None:
818
ie = self._get_file_ie(name, path, value, dir_ids[parent])
819
yield (posixpath.relpath(path, from_dir), "V", ie.kind, ie)
821
ie = fk_entries[kind]()
822
yield (posixpath.relpath(path, from_dir),
823
("I" if self.is_ignored(path) else "?"), kind, ie)
825
def all_file_ids(self):
826
raise errors.UnsupportedOperation(self.all_file_ids, self)
828
def all_versioned_paths(self):
829
with self.lock_read():
831
for path in self.index:
832
if self.mapping.is_special_file(path):
834
path = path.decode("utf-8")
837
path = posixpath.dirname(path).strip("/")
843
def iter_child_entries(self, path):
844
encoded_path = path.encode('utf-8')
845
with self.lock_read():
846
parent_id = self.path2id(path)
848
for item_path, value in self.index.iteritems():
849
decoded_item_path = item_path.decode('utf-8')
850
if self.mapping.is_special_file(item_path):
852
if not osutils.is_inside(path, decoded_item_path):
855
subpath = posixpath.relpath(decoded_item_path, path)
857
dirname = subpath.split('/', 1)[0]
858
file_ie = self._get_dir_ie(
859
posixpath.join(path, dirname), parent_id)
861
(unused_parent, name) = posixpath.split(decoded_item_path)
862
file_ie = self._get_file_ie(
863
name, decoded_item_path, value, parent_id)
865
if not found_any and path != u'':
866
raise errors.NoSuchFile(path)
869
with self.lock_read():
870
conflicts = _mod_conflicts.ConflictList()
871
for item_path, value in self.index.iteritems():
872
if value.flags & FLAG_STAGEMASK:
873
conflicts.append(_mod_conflicts.TextConflict(
874
item_path.decode('utf-8')))
877
def set_conflicts(self, conflicts):
879
for conflict in conflicts:
880
if conflict.typestring in ('text conflict', 'contents conflict'):
881
by_path.add(conflict.path.encode('utf-8'))
883
raise errors.UnsupportedOperation(self.set_conflicts, self)
884
with self.lock_tree_write():
885
for path in self.index:
886
self._set_conflicted(path, path in by_path)
888
def _set_conflicted(self, path, conflicted):
889
trace.mutter('change conflict: %r -> %r', path, conflicted)
890
value = self.index[path]
891
self._index_dirty = True
893
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
895
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
897
def add_conflicts(self, new_conflicts):
898
with self.lock_tree_write():
899
for conflict in new_conflicts:
900
if conflict.typestring in ('text conflict',
901
'contents conflict'):
903
self._set_conflicted(
904
conflict.path.encode('utf-8'), True)
906
raise errors.UnsupportedOperation(
907
self.add_conflicts, self)
909
raise errors.UnsupportedOperation(self.add_conflicts, self)
911
def walkdirs(self, prefix=""):
912
"""Walk the directories of this tree.
914
returns a generator which yields items in the form:
915
((curren_directory_path, fileid),
916
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
919
This API returns a generator, which is only valid during the current
920
tree transaction - within a single lock_read or lock_write duration.
922
If the tree is not locked, it may cause an error to be raised,
923
depending on the tree implementation.
925
from bisect import bisect_left
927
disk_top = self.abspath(prefix)
928
if disk_top.endswith('/'):
929
disk_top = disk_top[:-1]
930
top_strip_len = len(disk_top) + 1
931
inventory_iterator = self._walkdirs(prefix)
932
disk_iterator = osutils.walkdirs(disk_top, prefix)
934
current_disk = next(disk_iterator)
935
disk_finished = False
937
if not (e.errno == errno.ENOENT
938
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
943
current_inv = next(inventory_iterator)
945
except StopIteration:
948
while not inv_finished or not disk_finished:
950
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
951
cur_disk_dir_content) = current_disk
953
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
954
cur_disk_dir_content) = ((None, None), None)
955
if not disk_finished:
956
# strip out .bzr dirs
957
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
958
and len(cur_disk_dir_content) > 0):
959
# osutils.walkdirs can be made nicer -
960
# yield the path-from-prefix rather than the pathjoined
962
bzrdir_loc = bisect_left(cur_disk_dir_content,
964
if (bzrdir_loc < len(cur_disk_dir_content) and
965
self.controldir.is_control_filename(
966
cur_disk_dir_content[bzrdir_loc][0])):
967
# we dont yield the contents of, or, .bzr itself.
968
del cur_disk_dir_content[bzrdir_loc]
970
# everything is unknown
973
# everything is missing
976
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
977
- (current_inv[0][0] < cur_disk_dir_relpath))
979
# disk is before inventory - unknown
980
dirblock = [(relpath, basename, kind, stat, None, None) for
981
relpath, basename, kind, stat, top_path in
982
cur_disk_dir_content]
983
yield (cur_disk_dir_relpath, None), dirblock
985
current_disk = next(disk_iterator)
986
except StopIteration:
989
# inventory is before disk - missing.
990
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
991
for relpath, basename, dkind, stat, fileid, kind in
993
yield (current_inv[0][0], current_inv[0][1]), dirblock
995
current_inv = next(inventory_iterator)
996
except StopIteration:
999
# versioned present directory
1000
# merge the inventory and disk data together
1002
for relpath, subiterator in itertools.groupby(sorted(
1003
current_inv[1] + cur_disk_dir_content,
1004
key=operator.itemgetter(0)), operator.itemgetter(1)):
1005
path_elements = list(subiterator)
1006
if len(path_elements) == 2:
1007
inv_row, disk_row = path_elements
1008
# versioned, present file
1009
dirblock.append((inv_row[0],
1010
inv_row[1], disk_row[2],
1011
disk_row[3], inv_row[4],
1013
elif len(path_elements[0]) == 5:
1016
(path_elements[0][0], path_elements[0][1],
1017
path_elements[0][2], path_elements[0][3],
1019
elif len(path_elements[0]) == 6:
1020
# versioned, absent file.
1022
(path_elements[0][0], path_elements[0][1],
1023
'unknown', None, path_elements[0][4],
1024
path_elements[0][5]))
1026
raise NotImplementedError('unreachable code')
1027
yield current_inv[0], dirblock
1029
current_inv = next(inventory_iterator)
1030
except StopIteration:
1033
current_disk = next(disk_iterator)
1034
except StopIteration:
1035
disk_finished = True
1037
def _walkdirs(self, prefix=u""):
1040
prefix = prefix.encode('utf-8')
1041
per_dir = defaultdict(set)
1043
per_dir[(u'', self.get_root_id())] = set()
1045
def add_entry(path, kind):
1046
if path == b'' or not path.startswith(prefix):
1048
(dirname, child_name) = posixpath.split(path)
1049
add_entry(dirname, 'directory')
1050
dirname = dirname.decode("utf-8")
1051
dir_file_id = self.path2id(dirname)
1052
if not isinstance(value, tuple) or len(value) != 10:
1053
raise ValueError(value)
1054
per_dir[(dirname, dir_file_id)].add(
1055
(path.decode("utf-8"), child_name.decode("utf-8"),
1057
self.path2id(path.decode("utf-8")),
1059
with self.lock_read():
1060
for path, value in self.index.iteritems():
1061
if self.mapping.is_special_file(path):
1063
if not path.startswith(prefix):
1065
add_entry(path, mode_kind(value.mode))
1066
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1068
def get_shelf_manager(self):
1069
raise workingtree.ShelvingUnsupported()
1071
def store_uncommitted(self):
1072
raise errors.StoringUncommittedNotSupported(self)
1074
def apply_inventory_delta(self, changes):
1075
for (old_path, new_path, file_id, ie) in changes:
1076
if old_path is not None:
1077
(index, old_subpath) = self._lookup_index(
1078
old_path.encode('utf-8'))
1080
self._index_del_entry(index, old_subpath)
1084
self._versioned_dirs = None
1085
if new_path is not None and ie.kind != 'directory':
1086
if ie.kind == 'tree-reference':
1087
self._index_add_entry(
1089
reference_revision=ie.reference_revision)
1091
self._index_add_entry(new_path, ie.kind)
1094
def annotate_iter(self, path,
1095
default_revision=_mod_revision.CURRENT_REVISION):
1096
"""See Tree.annotate_iter
1098
This implementation will use the basis tree implementation if possible.
1099
Lines not in the basis are attributed to CURRENT_REVISION
1101
If there are pending merges, lines added by those merges will be
1102
incorrectly attributed to CURRENT_REVISION (but after committing, the
1103
attribution will be correct).
1105
with self.lock_read():
1106
maybe_file_parent_keys = []
1107
for parent_id in self.get_parent_ids():
1109
parent_tree = self.revision_tree(parent_id)
1110
except errors.NoSuchRevisionInTree:
1111
parent_tree = self.branch.repository.revision_tree(
1113
with parent_tree.lock_read():
1114
# TODO(jelmer): Use rename/copy tracker to find path name
1118
kind = parent_tree.kind(parent_path)
1119
except errors.NoSuchFile:
1122
# Note: this is slightly unnecessary, because symlinks
1123
# and directories have a "text" which is the empty
1124
# text, and we know that won't mess up annotations. But
1129
parent_tree.get_file_revision(parent_path))
1130
if parent_text_key not in maybe_file_parent_keys:
1131
maybe_file_parent_keys.append(parent_text_key)
1132
# Now we have the parents of this content
1133
from breezy.annotate import Annotator
1134
from .annotate import AnnotateProvider
1135
annotate_provider = AnnotateProvider(
1136
self.branch.repository._file_change_scanner)
1137
annotator = Annotator(annotate_provider)
1139
from breezy.graph import Graph
1140
graph = Graph(annotate_provider)
1141
heads = graph.heads(maybe_file_parent_keys)
1142
file_parent_keys = []
1143
for key in maybe_file_parent_keys:
1145
file_parent_keys.append(key)
1147
text = self.get_file_text(path)
1148
this_key = (path, default_revision)
1149
annotator.add_special_text(this_key, file_parent_keys, text)
1150
annotations = [(key[-1], line)
1151
for key, line in annotator.annotate_flat(this_key)]
1154
def _rename_one(self, from_rel, to_rel):
1155
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1157
def _build_checkout_with_index(self):
1158
build_index_from_tree(
1159
self.user_transport.local_abspath('.'),
1160
self.control_transport.local_abspath("index"),
1163
if self.branch.head is None
1164
else self.store[self.branch.head].tree,
1165
honor_filemode=self._supports_executable())
1167
def reset_state(self, revision_ids=None):
1168
"""Reset the state of the working tree.
1170
This does a hard-reset to a last-known-good state. This is a way to
1171
fix if something got corrupted (like the .git/index file)
1173
with self.lock_tree_write():
1174
if revision_ids is not None:
1175
self.set_parent_ids(revision_ids)
1177
self._index_dirty = True
1178
if self.branch.head is not None:
1179
for entry in self.store.iter_tree_contents(
1180
self.store[self.branch.head].tree):
1181
if not validate_path(entry.path):
1184
if S_ISGITLINK(entry.mode):
1185
pass # TODO(jelmer): record and return submodule paths
1187
# Let's at least try to use the working tree file:
1189
st = self._lstat(self.abspath(
1190
entry.path.decode('utf-8')))
1192
# But if it doesn't exist, we'll make something up.
1193
obj = self.store[entry.sha]
1194
st = os.stat_result((entry.mode, 0, 0, 0,
1196
obj.as_raw_string()), 0,
1198
(index, subpath) = self._lookup_index(entry.path)
1199
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1201
def pull(self, source, overwrite=False, stop_revision=None,
1202
change_reporter=None, possible_transports=None, local=False,
1204
with self.lock_write(), source.lock_read():
1205
old_revision = self.branch.last_revision()
1206
basis_tree = self.basis_tree()
1207
count = self.branch.pull(source, overwrite, stop_revision,
1208
possible_transports=possible_transports,
1210
new_revision = self.branch.last_revision()
1211
if new_revision != old_revision:
1212
with basis_tree.lock_read():
1213
new_basis_tree = self.branch.basis_tree()
1219
change_reporter=change_reporter,
1220
show_base=show_base)
1223
def add_reference(self, sub_tree):
1224
"""Add a TreeReference to the tree, pointing at sub_tree.
1226
:param sub_tree: subtree to add.
1228
with self.lock_tree_write():
1230
sub_tree_path = self.relpath(sub_tree.basedir)
1231
except errors.PathNotChild:
1232
raise BadReferenceTarget(
1233
self, sub_tree, 'Target not inside tree.')
1235
self._add([sub_tree_path], [None], ['tree-reference'])
1237
def _read_submodule_head(self, path):
1238
return read_submodule_head(self.abspath(path))
1240
def get_reference_revision(self, path):
1241
hexsha = self._read_submodule_head(path)
1243
return _mod_revision.NULL_REVISION
1244
return self.branch.lookup_foreign_revision_id(hexsha)
1246
def get_nested_tree(self, path):
1247
return workingtree.WorkingTree.open(self.abspath(path))
1249
def _directory_is_tree_reference(self, relpath):
1250
# as a special case, if a directory contains control files then
1251
# it's a tree reference, except that the root of the tree is not
1252
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1254
def extract(self, sub_path, format=None):
1255
"""Extract a subtree from this tree.
1257
A new branch will be created, relative to the path for this tree.
1260
segments = osutils.splitpath(path)
1261
transport = self.branch.controldir.root_transport
1262
for name in segments:
1263
transport = transport.clone(name)
1264
transport.ensure_base()
1267
with self.lock_tree_write():
1269
branch_transport = mkdirs(sub_path)
1271
format = self.controldir.cloning_metadir()
1272
branch_transport.ensure_base()
1273
branch_bzrdir = format.initialize_on_transport(branch_transport)
1275
repo = branch_bzrdir.find_repository()
1276
except errors.NoRepositoryPresent:
1277
repo = branch_bzrdir.create_repository()
1278
if not repo.supports_rich_root():
1279
raise errors.RootNotRich()
1280
new_branch = branch_bzrdir.create_branch()
1281
new_branch.pull(self.branch)
1282
for parent_id in self.get_parent_ids():
1283
new_branch.fetch(self.branch, parent_id)
1284
tree_transport = self.controldir.root_transport.clone(sub_path)
1285
if tree_transport.base != branch_transport.base:
1286
tree_bzrdir = format.initialize_on_transport(tree_transport)
1287
tree_bzrdir.set_branch_reference(new_branch)
1289
tree_bzrdir = branch_bzrdir
1290
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1291
wt.set_parent_ids(self.get_parent_ids())
1294
def _get_check_refs(self):
1295
"""Return the references needed to perform a check of this tree.
1297
The default implementation returns no refs, and is only suitable for
1298
trees that have no local caching and can commit on ghosts at any time.
1300
:seealso: breezy.check for details about check_refs.
1304
def copy_content_into(self, tree, revision_id=None):
1305
"""Copy the current content and user files of this tree into tree."""
1306
with self.lock_read():
1307
if revision_id is None:
1308
merge.transform_tree(tree, self)
1310
# TODO now merge from tree.last_revision to revision (to
1311
# preserve user local changes)
1313
other_tree = self.revision_tree(revision_id)
1314
except errors.NoSuchRevision:
1315
other_tree = self.branch.repository.revision_tree(
1318
merge.transform_tree(tree, other_tree)
1319
if revision_id == _mod_revision.NULL_REVISION:
1322
new_parents = [revision_id]
1323
tree.set_parent_ids(new_parents)
1326
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1328
_tree_class = GitWorkingTree
1330
supports_versioned_directories = False
1332
supports_setting_file_ids = False
1334
supports_store_uncommitted = False
1336
supports_leftmost_parent_id_as_ghost = False
1338
supports_righthand_parent_id_as_ghost = False
1340
requires_normalized_unicode_filenames = True
1342
supports_merge_modified = False
1344
ignore_filename = ".gitignore"
1347
def _matchingcontroldir(self):
1348
from .dir import LocalGitControlDirFormat
1349
return LocalGitControlDirFormat()
1351
def get_format_description(self):
1352
return "Git Working Tree"
1354
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1355
accelerator_tree=None, hardlink=False):
1356
"""See WorkingTreeFormat.initialize()."""
1357
if not isinstance(a_controldir, LocalGitDir):
1358
raise errors.IncompatibleFormat(self, a_controldir)
1359
branch = a_controldir.open_branch(nascent_ok=True)
1360
if revision_id is not None:
1361
branch.set_last_revision(revision_id)
1362
wt = GitWorkingTree(
1363
a_controldir, a_controldir.open_repository(), branch)
1364
for hook in MutableTree.hooks['post_build_tree']: