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 (
52
branch as _mod_branch,
53
conflicts as _mod_conflicts,
55
controldir as _mod_controldir,
59
revision as _mod_revision,
61
transport as _mod_transport,
66
from ..decorators import (
69
from ..mutabletree import (
81
from .mapping import (
88
CONFLICT_SUFFIXES = ['.BASE', '.OTHER', '.THIS']
91
# TODO: There should be a base revid attribute to better inform the user about
92
# how the conflicts were generated.
93
class TextConflict(_mod_conflicts.Conflict):
94
"""The merge algorithm could not resolve all differences encountered."""
98
typestring = 'text conflict'
100
_conflict_re = re.compile(b'^(<{7}|={7}|>{7})')
102
def associated_filenames(self):
103
return [self.path + suffix for suffix in CONFLICT_SUFFIXES]
105
def _resolve(self, tt, winner_suffix):
106
"""Resolve the conflict by copying one of .THIS or .OTHER into file.
108
:param tt: The TreeTransform where the conflict is resolved.
109
:param winner_suffix: Either 'THIS' or 'OTHER'
111
The resolution is symmetric, when taking THIS, item.THIS is renamed
112
into item and vice-versa. This takes one of the files as a whole
113
ignoring every difference that could have been merged cleanly.
115
# To avoid useless copies, we switch item and item.winner_suffix, only
116
# item will exist after the conflict has been resolved anyway.
117
item_tid = tt.trans_id_tree_path(self.path)
118
item_parent_tid = tt.get_tree_parent(item_tid)
119
winner_path = self.path + '.' + winner_suffix
120
winner_tid = tt.trans_id_tree_path(winner_path)
121
winner_parent_tid = tt.get_tree_parent(winner_tid)
122
# Switch the paths to preserve the content
123
tt.adjust_path(osutils.basename(self.path),
124
winner_parent_tid, winner_tid)
125
tt.adjust_path(osutils.basename(winner_path),
126
item_parent_tid, item_tid)
127
tt.unversion_file(item_tid)
128
tt.version_file(winner_tid)
131
def action_auto(self, tree):
132
# GZ 2012-07-27: Using NotImplementedError to signal that a conflict
133
# can't be auto resolved does not seem ideal.
135
kind = tree.kind(self.path)
136
except errors.NoSuchFile:
139
raise NotImplementedError("Conflict is not a file")
140
conflict_markers_in_line = self._conflict_re.search
141
with tree.get_file(self.path) as f:
143
if conflict_markers_in_line(line):
144
raise NotImplementedError("Conflict markers present")
146
def _resolve_with_cleanups(self, tree, *args, **kwargs):
147
with tree.transform() as tt:
148
self._resolve(tt, *args, **kwargs)
150
def action_take_this(self, tree):
151
self._resolve_with_cleanups(tree, 'THIS')
153
def action_take_other(self, tree):
154
self._resolve_with_cleanups(tree, 'OTHER')
156
def do(self, action, tree):
157
"""Apply the specified action to the conflict.
159
:param action: The method name to call.
161
:param tree: The tree passed as a parameter to the method.
163
meth = getattr(self, 'action_%s' % action, None)
165
raise NotImplementedError(self.__class__.__name__ + '.' + action)
168
def action_done(self, tree):
169
"""Mark the conflict as solved once it has been handled."""
170
# This method does nothing but simplifies the design of upper levels.
174
return 'Text conflict in %(path)s' % self.__dict__
177
return self.describe()
180
return "%s(%r)" % (type(self).__name__, self.path)
183
class GitWorkingTree(MutableGitIndexTree, workingtree.WorkingTree):
184
"""A Git working tree."""
186
def __init__(self, controldir, repo, branch):
187
MutableGitIndexTree.__init__(self)
188
basedir = controldir.root_transport.local_abspath('.')
189
self.basedir = osutils.realpath(basedir)
190
self.controldir = controldir
191
self.repository = repo
192
self.store = self.repository._git.object_store
193
self.mapping = self.repository.get_mapping()
194
self._branch = branch
195
self._transport = self.repository._git._controltransport
196
self._format = GitWorkingTreeFormat()
198
self._index_file = None
199
self.views = self._make_views()
200
self._rules_searcher = None
201
self._detect_case_handling()
204
def supports_tree_reference(self):
207
def supports_rename_tracking(self):
210
def _read_index(self):
211
self.index = Index(self.control_transport.local_abspath('index'))
212
self._index_dirty = False
214
def _get_submodule_index(self, relpath):
215
if not isinstance(relpath, bytes):
216
raise TypeError(relpath)
218
info = self._submodule_info()[relpath]
220
index_path = os.path.join(self.basedir, decode_git_path(relpath), '.git', 'index')
222
index_path = self.control_transport.local_abspath(
223
posixpath.join('modules', decode_git_path(info[1]), 'index'))
224
return Index(index_path)
227
"""Lock the repository for read operations.
229
:return: A breezy.lock.LogicalLockResult.
231
if not self._lock_mode:
232
self._lock_mode = 'r'
236
self._lock_count += 1
237
self.branch.lock_read()
238
return lock.LogicalLockResult(self.unlock)
240
def _lock_write_tree(self):
241
if not self._lock_mode:
242
self._lock_mode = 'w'
245
self._index_file = GitFile(
246
self.control_transport.local_abspath('index'), 'wb')
248
raise errors.LockContention('index')
250
elif self._lock_mode == 'r':
251
raise errors.ReadOnlyError(self)
253
self._lock_count += 1
255
def lock_tree_write(self):
256
self.branch.lock_read()
258
self._lock_write_tree()
259
return lock.LogicalLockResult(self.unlock)
260
except BaseException:
264
def lock_write(self, token=None):
265
self.branch.lock_write()
267
self._lock_write_tree()
268
return lock.LogicalLockResult(self.unlock)
269
except BaseException:
274
return self._lock_count >= 1
276
def get_physical_lock_status(self):
279
def break_lock(self):
281
self.control_transport.delete('index.lock')
282
except errors.NoSuchFile:
284
self.branch.break_lock()
286
@only_raises(errors.LockNotHeld, errors.LockBroken)
288
if not self._lock_count:
289
return lock.cant_unlock_not_held(self)
292
self._lock_count -= 1
293
if self._lock_count > 0:
295
if self._index_file is not None:
296
if self._index_dirty:
297
self._flush(self._index_file)
298
self._index_file.close()
300
# Something else already triggered a write of the index
301
# file by calling .flush()
302
self._index_file.abort()
303
self._index_file = None
304
self._lock_mode = None
312
def _detect_case_handling(self):
314
self._transport.stat(".git/cOnFiG")
315
except errors.NoSuchFile:
316
self.case_sensitive = True
318
self.case_sensitive = False
320
def merge_modified(self):
323
def set_merge_modified(self, modified_hashes):
324
raise errors.UnsupportedOperation(self.set_merge_modified, self)
326
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
327
self.set_parent_ids([p for p, t in parents_list])
329
def _set_merges_from_parent_ids(self, rhs_parent_ids):
331
merges = [self.branch.lookup_bzr_revision_id(
332
revid)[0] for revid in rhs_parent_ids]
333
except errors.NoSuchRevision as e:
334
raise errors.GhostRevisionUnusableHere(e.revision)
336
self.control_transport.put_bytes(
337
'MERGE_HEAD', b'\n'.join(merges),
338
mode=self.controldir._get_file_mode())
341
self.control_transport.delete('MERGE_HEAD')
342
except errors.NoSuchFile:
345
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
346
"""Set the parent ids to revision_ids.
348
See also set_parent_trees. This api will try to retrieve the tree data
349
for each element of revision_ids from the trees repository. If you have
350
tree data already available, it is more efficient to use
351
set_parent_trees rather than set_parent_ids. set_parent_ids is however
352
an easier API to use.
354
:param revision_ids: The revision_ids to set as the parent ids of this
355
working tree. Any of these may be ghosts.
357
with self.lock_tree_write():
358
self._check_parents_for_ghosts(
359
revision_ids, allow_leftmost_as_ghost=allow_leftmost_as_ghost)
360
for revision_id in revision_ids:
361
_mod_revision.check_not_reserved_id(revision_id)
363
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
365
if len(revision_ids) > 0:
366
self.set_last_revision(revision_ids[0])
368
self.set_last_revision(_mod_revision.NULL_REVISION)
370
self._set_merges_from_parent_ids(revision_ids[1:])
372
def get_parent_ids(self):
373
"""See Tree.get_parent_ids.
375
This implementation reads the pending merges list and last_revision
376
value and uses that to decide what the parents list should be.
378
last_rev = _mod_revision.ensure_null(self._last_revision())
379
if _mod_revision.NULL_REVISION == last_rev:
384
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
385
except errors.NoSuchFile:
388
for l in osutils.split_lines(merges_bytes):
389
revision_id = l.rstrip(b'\n')
391
self.branch.lookup_foreign_revision_id(revision_id))
394
def check_state(self):
395
"""Check that the working state is/isn't valid."""
398
def remove(self, files, verbose=False, to_file=None, keep_files=True,
400
"""Remove nominated files from the working tree metadata.
402
:param files: File paths relative to the basedir.
403
:param keep_files: If true, the files will also be kept.
404
:param force: Delete files and directories, even if they are changed
405
and even if the directories are not empty.
407
if not isinstance(files, list):
413
def backup(file_to_backup):
414
abs_path = self.abspath(file_to_backup)
415
backup_name = self.controldir._available_backup_name(
417
osutils.rename(abs_path, self.abspath(backup_name))
418
return "removed %s (but kept a copy: %s)" % (
419
file_to_backup, backup_name)
421
# Sort needed to first handle directory content before the directory
426
def recurse_directory_to_add_files(directory):
427
# Recurse directory and add all files
428
# so we can check if they have changed.
429
for parent_path, file_infos in self.walkdirs(directory):
430
for relpath, basename, kind, lstat, kind in file_infos:
431
# Is it versioned or ignored?
432
if self.is_versioned(relpath):
433
# Add nested content for deletion.
434
all_files.add(relpath)
436
# Files which are not versioned
437
# should be treated as unknown.
438
files_to_backup.append(relpath)
440
with self.lock_tree_write():
441
for filepath in files:
442
# Get file name into canonical form.
443
abspath = self.abspath(filepath)
444
filepath = self.relpath(abspath)
447
all_files.add(filepath)
448
recurse_directory_to_add_files(filepath)
450
files = list(all_files)
453
return # nothing to do
455
# Sort needed to first handle directory content before the
457
files.sort(reverse=True)
459
# Bail out if we are going to delete files we shouldn't
460
if not keep_files and not force:
461
for change in self.iter_changes(
462
self.basis_tree(), include_unchanged=True,
463
require_versioned=False, want_unversioned=True,
464
specific_files=files):
465
if change.versioned[0] is False:
466
# The record is unknown or newly added
467
files_to_backup.append(change.path[1])
468
files_to_backup.extend(
469
osutils.parent_directories(change.path[1]))
470
elif (change.changed_content and (change.kind[1] is not None)
471
and osutils.is_inside_any(files, change.path[1])):
472
# Versioned and changed, but not deleted, and still
473
# in one of the dirs to be deleted.
474
files_to_backup.append(change.path[1])
475
files_to_backup.extend(
476
osutils.parent_directories(change.path[1]))
484
except errors.NoSuchFile:
487
abs_path = self.abspath(f)
489
# having removed it, it must be either ignored or unknown
490
if self.is_ignored(f):
494
kind_ch = osutils.kind_marker(kind)
495
to_file.write(new_status + ' ' + f + kind_ch + '\n')
497
message = "%s does not exist" % (f, )
500
if f in files_to_backup and not force:
503
if kind == 'directory':
504
osutils.rmtree(abs_path)
506
osutils.delete_any(abs_path)
507
message = "deleted %s" % (f,)
509
message = "removed %s" % (f,)
510
self._unversion_path(f)
512
# print only one message (if any) per file.
513
if message is not None:
515
self._versioned_dirs = None
517
def smart_add(self, file_list, recurse=True, action=None, save=True):
521
# expand any symlinks in the directory part, while leaving the
523
# only expanding if symlinks are supported avoids windows path bugs
524
if self.supports_symlinks():
525
file_list = list(map(osutils.normalizepath, file_list))
527
conflicts_related = set()
528
for c in self.conflicts():
529
conflicts_related.update(c.associated_filenames())
535
def call_action(filepath, kind):
538
if action is not None:
539
parent_path = posixpath.dirname(filepath)
540
parent_id = self.path2id(parent_path)
541
parent_ie = self._get_dir_ie(parent_path, parent_id)
542
file_id = action(self, parent_ie, filepath, kind)
543
if file_id is not None:
544
raise workingtree.SettingFileIdUnsupported()
546
with self.lock_tree_write():
547
for filepath in osutils.canonical_relpaths(
548
self.basedir, file_list):
549
filepath, can_access = osutils.normalized_filename(filepath)
551
raise errors.InvalidNormalization(filepath)
553
abspath = self.abspath(filepath)
554
kind = osutils.file_kind(abspath)
555
if kind in ("file", "symlink"):
556
(index, subpath) = self._lookup_index(
557
encode_git_path(filepath))
561
call_action(filepath, kind)
563
self._index_add_entry(filepath, kind)
564
added.append(filepath)
565
elif kind == "directory":
566
(index, subpath) = self._lookup_index(
567
encode_git_path(filepath))
568
if subpath not in index:
569
call_action(filepath, kind)
571
user_dirs.append(filepath)
573
raise errors.BadFileKindError(filename=abspath, kind=kind)
574
for user_dir in user_dirs:
575
abs_user_dir = self.abspath(user_dir)
578
transport = _mod_transport.get_transport_from_path(
580
_mod_controldir.ControlDirFormat.find_format(transport)
582
except errors.NotBranchError:
584
except errors.UnsupportedFormatError:
589
trace.warning('skipping nested tree %r', abs_user_dir)
592
for name in os.listdir(abs_user_dir):
593
subp = os.path.join(user_dir, name)
594
if (self.is_control_filename(subp) or
595
self.mapping.is_special_file(subp)):
597
ignore_glob = self.is_ignored(subp)
598
if ignore_glob is not None:
599
ignored.setdefault(ignore_glob, []).append(subp)
601
abspath = self.abspath(subp)
602
kind = osutils.file_kind(abspath)
603
if kind == "directory":
604
user_dirs.append(subp)
606
(index, subpath) = self._lookup_index(
607
encode_git_path(subp))
611
if subp in conflicts_related:
613
call_action(subp, kind)
615
self._index_add_entry(subp, kind)
617
return added, ignored
619
def has_filename(self, filename):
620
return osutils.lexists(self.abspath(filename))
622
def _iter_files_recursive(self, from_dir=None, include_dirs=False,
623
recurse_nested=False):
626
if not isinstance(from_dir, str):
627
raise TypeError(from_dir)
628
encoded_from_dir = self.abspath(from_dir).encode(osutils._fs_enc)
629
for (dirpath, dirnames, filenames) in os.walk(encoded_from_dir):
630
dir_relpath = dirpath[len(self.basedir):].strip(b"/")
631
if self.controldir.is_control_filename(
632
dir_relpath.decode(osutils._fs_enc)):
634
for name in list(dirnames):
635
if self.controldir.is_control_filename(
636
name.decode(osutils._fs_enc)):
637
dirnames.remove(name)
639
relpath = os.path.join(dir_relpath, name)
640
if not recurse_nested and self._directory_is_tree_reference(relpath.decode(osutils._fs_enc)):
641
dirnames.remove(name)
644
yield relpath.decode(osutils._fs_enc)
645
except UnicodeDecodeError:
646
raise errors.BadFilenameEncoding(
647
relpath, osutils._fs_enc)
648
if not self.is_versioned(relpath.decode(osutils._fs_enc)):
649
dirnames.remove(name)
650
for name in filenames:
651
if self.mapping.is_special_file(name):
653
if self.controldir.is_control_filename(
654
name.decode(osutils._fs_enc, 'replace')):
656
yp = os.path.join(dir_relpath, name)
658
yield yp.decode(osutils._fs_enc)
659
except UnicodeDecodeError:
660
raise errors.BadFilenameEncoding(
664
"""Yield all unversioned files in this WorkingTree.
666
with self.lock_read():
668
[decode_git_path(p) for p, sha, mode in self.iter_git_objects()])
669
all_paths = set(self._iter_files_recursive(include_dirs=False))
670
return iter(all_paths - index_paths)
672
def _gather_kinds(self, files, kinds):
673
"""See MutableTree._gather_kinds."""
674
with self.lock_tree_write():
675
for pos, f in enumerate(files):
676
if kinds[pos] is None:
677
fullpath = osutils.normpath(self.abspath(f))
679
kind = osutils.file_kind(fullpath)
681
if e.errno == errno.ENOENT:
682
raise errors.NoSuchFile(fullpath)
683
if f != '' and self._directory_is_tree_reference(f):
684
kind = 'tree-reference'
688
if self._lock_mode != 'w':
689
raise errors.NotWriteLocked(self)
690
# TODO(jelmer): This shouldn't be writing in-place, but index.lock is
691
# already in use and GitFile doesn't allow overriding the lock file
693
f = open(self.control_transport.local_abspath('index'), 'wb')
694
# Note that _flush will close the file
700
write_index_dict(shaf, self.index)
702
except BaseException:
705
self._index_dirty = False
707
def get_file_mtime(self, path):
708
"""See Tree.get_file_mtime."""
710
return self._lstat(path).st_mtime
712
if e.errno == errno.ENOENT:
713
raise errors.NoSuchFile(path)
716
def is_ignored(self, filename):
717
r"""Check whether the filename matches an ignore pattern.
719
If the file is ignored, returns the pattern which caused it to
720
be ignored, otherwise None. So this can simply be used as a
721
boolean if desired."""
722
if getattr(self, '_global_ignoreglobster', None) is None:
723
from breezy import ignores
725
ignore_globs.update(ignores.get_runtime_ignores())
726
ignore_globs.update(ignores.get_user_ignores())
727
self._global_ignoreglobster = globbing.ExceptionGlobster(
729
match = self._global_ignoreglobster.match(filename)
730
if match is not None:
733
if self.kind(filename) == 'directory':
735
except errors.NoSuchFile:
737
filename = filename.lstrip('/')
738
ignore_manager = self._get_ignore_manager()
739
ps = list(ignore_manager.find_matching(filename))
742
if not ps[-1].is_exclude:
746
def _get_ignore_manager(self):
747
ignoremanager = getattr(self, '_ignoremanager', None)
748
if ignoremanager is not None:
751
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
752
self._ignoremanager = ignore_manager
753
return ignore_manager
755
def _flush_ignore_list_cache(self):
756
self._ignoremanager = None
758
def set_last_revision(self, revid):
759
if _mod_revision.is_null(revid):
760
self.branch.set_last_revision_info(0, revid)
762
_mod_revision.check_not_reserved_id(revid)
764
self.branch.generate_revision_history(revid)
765
except errors.NoSuchRevision:
766
raise errors.GhostRevisionUnusableHere(revid)
768
def _reset_data(self):
771
def get_file_verifier(self, path, stat_value=None):
772
with self.lock_read():
773
(index, subpath) = self._lookup_index(encode_git_path(path))
775
return ("GIT", index[subpath].sha)
777
if self._has_dir(path):
779
raise errors.NoSuchFile(path)
781
def get_file_sha1(self, path, stat_value=None):
782
with self.lock_read():
783
if not self.is_versioned(path):
784
raise errors.NoSuchFile(path)
785
abspath = self.abspath(path)
787
return osutils.sha_file_by_name(abspath)
789
if e.errno in (errno.EISDIR, errno.ENOENT):
793
def revision_tree(self, revid):
794
return self.repository.revision_tree(revid)
796
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
797
mode = stat_result.st_mode
798
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
800
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
801
return self.basis_tree().is_executable(path)
803
def stored_kind(self, path):
804
with self.lock_read():
805
encoded_path = encode_git_path(path)
806
(index, subpath) = self._lookup_index(encoded_path)
808
return mode_kind(index[subpath].mode)
810
# Maybe it's a directory?
811
if self._has_dir(encoded_path):
813
raise errors.NoSuchFile(path)
815
def _lstat(self, path):
816
return os.lstat(self.abspath(path))
818
def _live_entry(self, path):
819
encoded_path = self.abspath(decode_git_path(path)).encode(
821
return index_entry_from_path(encoded_path)
823
def is_executable(self, path):
824
with self.lock_read():
825
if self._supports_executable():
826
mode = self._lstat(path).st_mode
828
(index, subpath) = self._lookup_index(encode_git_path(path))
830
mode = index[subpath].mode
833
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
835
def _is_executable_from_path_and_stat(self, path, stat_result):
836
if self._supports_executable():
837
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
839
return self._is_executable_from_path_and_stat_from_basis(
842
def list_files(self, include_root=False, from_dir=None, recursive=True,
843
recurse_nested=False):
844
if from_dir is None or from_dir == '.':
847
fk_entries = {'directory': tree.TreeDirectory,
848
'file': tree.TreeFile,
849
'symlink': tree.TreeLink,
850
'tree-reference': tree.TreeReference}
851
with self.lock_read():
852
root_ie = self._get_dir_ie(u"", None)
853
if include_root and not from_dir:
854
yield "", "V", root_ie.kind, root_ie
855
dir_ids[u""] = root_ie.file_id
857
path_iterator = sorted(
858
self._iter_files_recursive(
859
from_dir, include_dirs=True,
860
recurse_nested=recurse_nested))
862
encoded_from_dir = self.abspath(from_dir).encode(
864
path_iterator = sorted(
865
[os.path.join(from_dir, name.decode(osutils._fs_enc))
866
for name in os.listdir(encoded_from_dir)
867
if not self.controldir.is_control_filename(
868
name.decode(osutils._fs_enc)) and
869
not self.mapping.is_special_file(
870
name.decode(osutils._fs_enc))])
871
for path in path_iterator:
873
encoded_path = encode_git_path(path)
874
except UnicodeEncodeError:
875
raise errors.BadFilenameEncoding(
876
path, osutils._fs_enc)
877
(index, index_path) = self._lookup_index(encoded_path)
879
value = index[index_path]
882
kind = self.kind(path)
883
parent, name = posixpath.split(path)
884
for dir_path, dir_ie in self._add_missing_parent_ids(
887
if kind == 'tree-reference' and recurse_nested:
888
ie = self._get_dir_ie(path, self.path2id(path))
889
yield (posixpath.relpath(path, from_dir), 'V', 'directory',
892
if kind == 'directory':
894
if self._has_dir(encoded_path):
895
ie = self._get_dir_ie(path, self.path2id(path))
897
elif self.is_ignored(path):
899
ie = fk_entries[kind]()
902
ie = fk_entries[kind]()
903
yield (posixpath.relpath(path, from_dir), status, kind,
906
if value is not None:
907
ie = self._get_file_ie(name, path, value, dir_ids[parent])
908
yield (posixpath.relpath(path, from_dir), "V", ie.kind, ie)
911
ie = fk_entries[kind]()
915
yield (posixpath.relpath(path, from_dir),
916
("I" if self.is_ignored(path) else "?"), kind, ie)
918
def all_file_ids(self):
919
raise errors.UnsupportedOperation(self.all_file_ids, self)
921
def all_versioned_paths(self):
922
with self.lock_read():
924
for path in self.index:
925
if self.mapping.is_special_file(path):
927
path = decode_git_path(path)
930
path = posixpath.dirname(path).strip("/")
936
def iter_child_entries(self, path):
937
encoded_path = encode_git_path(path)
938
with self.lock_read():
939
parent_id = self.path2id(path)
941
for item_path, value in self.index.iteritems():
942
decoded_item_path = decode_git_path(item_path)
943
if self.mapping.is_special_file(item_path):
945
if not osutils.is_inside(path, decoded_item_path):
948
subpath = posixpath.relpath(decoded_item_path, path)
950
dirname = subpath.split('/', 1)[0]
951
file_ie = self._get_dir_ie(
952
posixpath.join(path, dirname), parent_id)
954
(unused_parent, name) = posixpath.split(decoded_item_path)
955
file_ie = self._get_file_ie(
956
name, decoded_item_path, value, parent_id)
958
if not found_any and path != u'':
959
raise errors.NoSuchFile(path)
962
with self.lock_read():
963
conflicts = _mod_conflicts.ConflictList()
964
for item_path, value in self.index.iteritems():
965
if value.flags & FLAG_STAGEMASK:
966
conflicts.append(TextConflict(decode_git_path(item_path)))
969
def set_conflicts(self, conflicts):
971
for conflict in conflicts:
972
if conflict.typestring in ('text conflict', 'contents conflict'):
973
by_path.add(encode_git_path(conflict.path))
975
raise errors.UnsupportedOperation(self.set_conflicts, self)
976
with self.lock_tree_write():
977
for path in self.index:
978
self._set_conflicted(path, path in by_path)
980
def _set_conflicted(self, path, conflicted):
981
value = self.index[path]
982
self._index_dirty = True
984
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
986
self.index[path] = (value[:9] + (value[9] & ~ FLAG_STAGEMASK, ))
988
def add_conflicts(self, new_conflicts):
989
with self.lock_tree_write():
990
for conflict in new_conflicts:
991
if conflict.typestring in ('text conflict',
992
'contents conflict'):
994
self._set_conflicted(
995
encode_git_path(conflict.path), True)
997
raise errors.UnsupportedOperation(
998
self.add_conflicts, self)
1000
raise errors.UnsupportedOperation(self.add_conflicts, self)
1002
def walkdirs(self, prefix=""):
1003
"""Walk the directories of this tree.
1005
returns a generator which yields items in the form:
1006
(current_directory_path,
1007
[(file1_path, file1_name, file1_kind, (lstat),
1010
This API returns a generator, which is only valid during the current
1011
tree transaction - within a single lock_read or lock_write duration.
1013
If the tree is not locked, it may cause an error to be raised,
1014
depending on the tree implementation.
1016
from bisect import bisect_left
1018
disk_top = self.abspath(prefix)
1019
if disk_top.endswith('/'):
1020
disk_top = disk_top[:-1]
1021
top_strip_len = len(disk_top) + 1
1022
inventory_iterator = self._walkdirs(prefix)
1023
disk_iterator = osutils.walkdirs(disk_top, prefix)
1025
current_disk = next(disk_iterator)
1026
disk_finished = False
1027
except OSError as e:
1028
if not (e.errno == errno.ENOENT
1029
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
1032
disk_finished = True
1034
current_inv = next(inventory_iterator)
1035
inv_finished = False
1036
except StopIteration:
1039
while not inv_finished or not disk_finished:
1041
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1042
cur_disk_dir_content) = current_disk
1044
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1045
cur_disk_dir_content) = ((None, None), None)
1046
if not disk_finished:
1047
# strip out .bzr dirs
1048
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
1049
and len(cur_disk_dir_content) > 0):
1050
# osutils.walkdirs can be made nicer -
1051
# yield the path-from-prefix rather than the pathjoined
1053
bzrdir_loc = bisect_left(cur_disk_dir_content,
1055
if (bzrdir_loc < len(cur_disk_dir_content) and
1056
self.controldir.is_control_filename(
1057
cur_disk_dir_content[bzrdir_loc][0])):
1058
# we dont yield the contents of, or, .bzr itself.
1059
del cur_disk_dir_content[bzrdir_loc]
1061
# everything is unknown
1064
# everything is missing
1067
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
1068
- (current_inv[0][0] < cur_disk_dir_relpath))
1070
# disk is before inventory - unknown
1071
dirblock = [(relpath, basename, kind, stat, None) for
1072
relpath, basename, kind, stat, top_path in
1073
cur_disk_dir_content]
1074
yield cur_disk_dir_relpath, dirblock
1076
current_disk = next(disk_iterator)
1077
except StopIteration:
1078
disk_finished = True
1080
# inventory is before disk - missing.
1081
dirblock = [(relpath, basename, 'unknown', None, kind)
1082
for relpath, basename, dkind, stat, fileid, kind in
1084
yield current_inv[0][0], dirblock
1086
current_inv = next(inventory_iterator)
1087
except StopIteration:
1090
# versioned present directory
1091
# merge the inventory and disk data together
1093
for relpath, subiterator in itertools.groupby(sorted(
1094
current_inv[1] + cur_disk_dir_content,
1095
key=operator.itemgetter(0)), operator.itemgetter(1)):
1096
path_elements = list(subiterator)
1097
if len(path_elements) == 2:
1098
inv_row, disk_row = path_elements
1099
# versioned, present file
1100
dirblock.append((inv_row[0],
1101
inv_row[1], disk_row[2],
1102
disk_row[3], inv_row[5]))
1103
elif len(path_elements[0]) == 5:
1106
(path_elements[0][0], path_elements[0][1],
1107
path_elements[0][2], path_elements[0][3],
1109
elif len(path_elements[0]) == 6:
1110
# versioned, absent file.
1112
(path_elements[0][0], path_elements[0][1],
1114
path_elements[0][5]))
1116
raise NotImplementedError('unreachable code')
1117
yield current_inv[0][0], dirblock
1119
current_inv = next(inventory_iterator)
1120
except StopIteration:
1123
current_disk = next(disk_iterator)
1124
except StopIteration:
1125
disk_finished = True
1127
def _walkdirs(self, prefix=u""):
1130
prefix = encode_git_path(prefix)
1131
per_dir = defaultdict(set)
1133
per_dir[(u'', self.path2id(''))] = set()
1135
def add_entry(path, kind):
1136
if path == b'' or not path.startswith(prefix):
1138
(dirname, child_name) = posixpath.split(path)
1139
add_entry(dirname, 'directory')
1140
dirname = decode_git_path(dirname)
1141
dir_file_id = self.path2id(dirname)
1142
if not isinstance(value, tuple) or len(value) != 10:
1143
raise ValueError(value)
1144
per_dir[(dirname, dir_file_id)].add(
1145
(decode_git_path(path), decode_git_path(child_name),
1147
self.path2id(decode_git_path(path)),
1149
with self.lock_read():
1150
for path, value in self.index.iteritems():
1151
if self.mapping.is_special_file(path):
1153
if not path.startswith(prefix):
1155
add_entry(path, mode_kind(value.mode))
1156
return ((k, sorted(v)) for (k, v) in sorted(per_dir.items()))
1158
def get_shelf_manager(self):
1159
raise workingtree.ShelvingUnsupported()
1161
def store_uncommitted(self):
1162
raise errors.StoringUncommittedNotSupported(self)
1164
def annotate_iter(self, path,
1165
default_revision=_mod_revision.CURRENT_REVISION):
1166
"""See Tree.annotate_iter
1168
This implementation will use the basis tree implementation if possible.
1169
Lines not in the basis are attributed to CURRENT_REVISION
1171
If there are pending merges, lines added by those merges will be
1172
incorrectly attributed to CURRENT_REVISION (but after committing, the
1173
attribution will be correct).
1175
with self.lock_read():
1176
maybe_file_parent_keys = []
1177
for parent_id in self.get_parent_ids():
1179
parent_tree = self.revision_tree(parent_id)
1180
except errors.NoSuchRevisionInTree:
1181
parent_tree = self.branch.repository.revision_tree(
1183
with parent_tree.lock_read():
1184
# TODO(jelmer): Use rename/copy tracker to find path name
1188
kind = parent_tree.kind(parent_path)
1189
except errors.NoSuchFile:
1192
# Note: this is slightly unnecessary, because symlinks
1193
# and directories have a "text" which is the empty
1194
# text, and we know that won't mess up annotations. But
1199
parent_tree.get_file_revision(parent_path))
1200
if parent_text_key not in maybe_file_parent_keys:
1201
maybe_file_parent_keys.append(parent_text_key)
1202
# Now we have the parents of this content
1203
from breezy.annotate import Annotator
1204
from .annotate import AnnotateProvider
1205
annotate_provider = AnnotateProvider(
1206
self.branch.repository._file_change_scanner)
1207
annotator = Annotator(annotate_provider)
1209
from breezy.graph import Graph
1210
graph = Graph(annotate_provider)
1211
heads = graph.heads(maybe_file_parent_keys)
1212
file_parent_keys = []
1213
for key in maybe_file_parent_keys:
1215
file_parent_keys.append(key)
1217
text = self.get_file_text(path)
1218
this_key = (path, default_revision)
1219
annotator.add_special_text(this_key, file_parent_keys, text)
1220
annotations = [(key[-1], line)
1221
for key, line in annotator.annotate_flat(this_key)]
1224
def _rename_one(self, from_rel, to_rel):
1225
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1227
def _build_checkout_with_index(self):
1228
build_index_from_tree(
1229
self.user_transport.local_abspath('.'),
1230
self.control_transport.local_abspath("index"),
1233
if self.branch.head is None
1234
else self.store[self.branch.head].tree,
1235
honor_filemode=self._supports_executable())
1237
def reset_state(self, revision_ids=None):
1238
"""Reset the state of the working tree.
1240
This does a hard-reset to a last-known-good state. This is a way to
1241
fix if something got corrupted (like the .git/index file)
1243
with self.lock_tree_write():
1244
if revision_ids is not None:
1245
self.set_parent_ids(revision_ids)
1247
self._index_dirty = True
1248
if self.branch.head is not None:
1249
for entry in self.store.iter_tree_contents(
1250
self.store[self.branch.head].tree):
1251
if not validate_path(entry.path):
1254
if S_ISGITLINK(entry.mode):
1255
pass # TODO(jelmer): record and return submodule paths
1257
# Let's at least try to use the working tree file:
1259
st = self._lstat(self.abspath(
1260
decode_git_path(entry.path)))
1262
# But if it doesn't exist, we'll make something up.
1263
obj = self.store[entry.sha]
1264
st = os.stat_result((entry.mode, 0, 0, 0,
1266
obj.as_raw_string()), 0,
1268
(index, subpath) = self._lookup_index(entry.path)
1269
index[subpath] = index_entry_from_stat(st, entry.sha, 0)
1271
def _update_git_tree(
1272
self, old_revision, new_revision, change_reporter=None,
1274
basis_tree = self.revision_tree(old_revision)
1275
if new_revision != old_revision:
1276
from .. import merge
1277
with basis_tree.lock_read():
1278
new_basis_tree = self.branch.basis_tree()
1284
change_reporter=change_reporter,
1285
show_base=show_base)
1287
def pull(self, source, overwrite=False, stop_revision=None,
1288
change_reporter=None, possible_transports=None, local=False,
1289
show_base=False, tag_selector=None):
1290
with self.lock_write(), source.lock_read():
1291
old_revision = self.branch.last_revision()
1292
count = self.branch.pull(source, overwrite, stop_revision,
1293
possible_transports=possible_transports,
1294
local=local, tag_selector=tag_selector)
1295
self._update_git_tree(
1296
old_revision=old_revision,
1297
new_revision=self.branch.last_revision(),
1298
change_reporter=change_reporter,
1299
show_base=show_base)
1302
def add_reference(self, sub_tree):
1303
"""Add a TreeReference to the tree, pointing at sub_tree.
1305
:param sub_tree: subtree to add.
1307
with self.lock_tree_write():
1309
sub_tree_path = self.relpath(sub_tree.basedir)
1310
except errors.PathNotChild:
1311
raise BadReferenceTarget(
1312
self, sub_tree, 'Target not inside tree.')
1314
self._add([sub_tree_path], [None], ['tree-reference'])
1316
def _read_submodule_head(self, path):
1317
return read_submodule_head(self.abspath(path))
1319
def get_reference_revision(self, path, branch=None):
1320
hexsha = self._read_submodule_head(path)
1322
(index, subpath) = self._lookup_index(
1323
encode_git_path(path))
1325
raise errors.NoSuchFile(path)
1326
hexsha = index[subpath].sha
1327
return self.branch.lookup_foreign_revision_id(hexsha)
1329
def get_nested_tree(self, path):
1330
return workingtree.WorkingTree.open(self.abspath(path))
1332
def _directory_is_tree_reference(self, relpath):
1333
# as a special case, if a directory contains control files then
1334
# it's a tree reference, except that the root of the tree is not
1335
return relpath and osutils.lexists(self.abspath(relpath) + u"/.git")
1337
def extract(self, sub_path, format=None):
1338
"""Extract a subtree from this tree.
1340
A new branch will be created, relative to the path for this tree.
1343
segments = osutils.splitpath(path)
1344
transport = self.branch.controldir.root_transport
1345
for name in segments:
1346
transport = transport.clone(name)
1347
transport.ensure_base()
1350
with self.lock_tree_write():
1352
branch_transport = mkdirs(sub_path)
1354
format = self.controldir.cloning_metadir()
1355
branch_transport.ensure_base()
1356
branch_bzrdir = format.initialize_on_transport(branch_transport)
1358
repo = branch_bzrdir.find_repository()
1359
except errors.NoRepositoryPresent:
1360
repo = branch_bzrdir.create_repository()
1361
if not repo.supports_rich_root():
1362
raise errors.RootNotRich()
1363
new_branch = branch_bzrdir.create_branch()
1364
new_branch.pull(self.branch)
1365
for parent_id in self.get_parent_ids():
1366
new_branch.fetch(self.branch, parent_id)
1367
tree_transport = self.controldir.root_transport.clone(sub_path)
1368
if tree_transport.base != branch_transport.base:
1369
tree_bzrdir = format.initialize_on_transport(tree_transport)
1370
tree_bzrdir.set_branch_reference(new_branch)
1372
tree_bzrdir = branch_bzrdir
1373
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
1374
wt.set_parent_ids(self.get_parent_ids())
1377
def _get_check_refs(self):
1378
"""Return the references needed to perform a check of this tree.
1380
The default implementation returns no refs, and is only suitable for
1381
trees that have no local caching and can commit on ghosts at any time.
1383
:seealso: breezy.check for details about check_refs.
1387
def copy_content_into(self, tree, revision_id=None):
1388
"""Copy the current content and user files of this tree into tree."""
1389
from .. import merge
1390
with self.lock_read():
1391
if revision_id is None:
1392
merge.transform_tree(tree, self)
1394
# TODO now merge from tree.last_revision to revision (to
1395
# preserve user local changes)
1397
other_tree = self.revision_tree(revision_id)
1398
except errors.NoSuchRevision:
1399
other_tree = self.branch.repository.revision_tree(
1402
merge.transform_tree(tree, other_tree)
1403
if revision_id == _mod_revision.NULL_REVISION:
1406
new_parents = [revision_id]
1407
tree.set_parent_ids(new_parents)
1409
def reference_parent(self, path, possible_transports=None):
1410
remote_url = self.get_reference_info(path)
1411
if remote_url is None:
1412
trace.warning("Unable to find submodule info for %s", path)
1414
return _mod_branch.Branch.open(remote_url, possible_transports=possible_transports)
1416
def get_reference_info(self, path):
1417
submodule_info = self._submodule_info()
1418
info = submodule_info.get(encode_git_path(path))
1421
return decode_git_path(info[0])
1423
def set_reference_info(self, tree_path, branch_location):
1424
path = self.abspath('.gitmodules')
1426
config = GitConfigFile.from_path(path)
1427
except EnvironmentError as e:
1428
if e.errno == errno.ENOENT:
1429
config = GitConfigFile()
1432
section = (b'submodule', encode_git_path(tree_path))
1433
if branch_location is None:
1439
branch_location = urlutils.join(
1440
urlutils.strip_segment_parameters(self.branch.user_url),
1444
b'path', encode_git_path(tree_path))
1447
b'url', branch_location.encode('utf-8'))
1448
config.write_to_path(path)
1449
self.add('.gitmodules')
1452
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1454
_tree_class = GitWorkingTree
1456
supports_versioned_directories = False
1458
supports_setting_file_ids = False
1460
supports_store_uncommitted = False
1462
supports_leftmost_parent_id_as_ghost = False
1464
supports_righthand_parent_id_as_ghost = False
1466
requires_normalized_unicode_filenames = True
1468
supports_merge_modified = False
1470
ignore_filename = ".gitignore"
1473
def _matchingcontroldir(self):
1474
from .dir import LocalGitControlDirFormat
1475
return LocalGitControlDirFormat()
1477
def get_format_description(self):
1478
return "Git Working Tree"
1480
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1481
accelerator_tree=None, hardlink=False):
1482
"""See WorkingTreeFormat.initialize()."""
1483
if not isinstance(a_controldir, LocalGitDir):
1484
raise errors.IncompatibleFormat(self, a_controldir)
1485
branch = a_controldir.open_branch(nascent_ok=True)
1486
if revision_id is not None:
1487
branch.set_last_revision(revision_id)
1488
wt = GitWorkingTree(
1489
a_controldir, a_controldir.open_repository(), branch)
1490
for hook in MutableTree.hooks['post_build_tree']: