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 cStringIO import (
26
from collections import defaultdict
28
from dulwich.errors import NotGitRepository
29
from dulwich.ignore import (
32
from dulwich.index import (
34
build_index_from_tree,
38
index_entry_from_stat,
40
blob_from_path_and_stat,
44
from dulwich.object_store import (
47
from dulwich.objects import (
53
from dulwich.repo import Repo
61
conflicts as _mod_conflicts,
63
controldir as _mod_controldir,
69
revision as _mod_revision,
71
transport as _mod_transport,
78
from ...mutabletree import (
87
changes_from_git_changes,
88
tree_delta_from_git_changes,
92
from .mapping import (
97
IGNORE_FILENAME = ".gitignore"
100
class GitWorkingTree(MutableGitIndexTree,workingtree.WorkingTree):
101
"""A Git working tree."""
103
def __init__(self, controldir, repo, branch, index):
104
MutableGitIndexTree.__init__(self)
105
basedir = controldir.root_transport.local_abspath('.')
106
self.basedir = osutils.realpath(basedir)
107
self.controldir = controldir
108
self.repository = repo
109
self.store = self.repository._git.object_store
110
self.mapping = self.repository.get_mapping()
111
self._branch = branch
112
self._transport = controldir.transport
113
self._format = GitWorkingTreeFormat()
115
self.views = self._make_views()
116
self._rules_searcher = None
117
self._detect_case_handling()
120
def supports_tree_reference(self):
123
def supports_rename_tracking(self):
127
"""Lock the repository for read operations.
129
:return: A breezy.lock.LogicalLockResult.
131
if not self._lock_mode:
132
self._lock_mode = 'r'
136
self._lock_count += 1
137
self.branch.lock_read()
138
return lock.LogicalLockResult(self.unlock)
140
def lock_tree_write(self):
141
if not self._lock_mode:
142
self._lock_mode = 'w'
145
elif self._lock_mode == 'r':
146
raise errors.ReadOnlyError(self)
149
self.branch.lock_read()
150
return lock.LogicalLockResult(self.unlock)
152
def lock_write(self, token=None):
153
if not self._lock_mode:
154
self._lock_mode = 'w'
157
elif self._lock_mode == 'r':
158
raise errors.ReadOnlyError(self)
161
self.branch.lock_write()
162
return lock.LogicalLockResult(self.unlock)
165
return self._lock_count >= 1
167
def get_physical_lock_status(self):
171
if not self._lock_count:
172
return lock.cant_unlock_not_held(self)
175
self._lock_count -= 1
176
if self._lock_count > 0:
178
self._lock_mode = None
186
def _detect_case_handling(self):
188
self._transport.stat(".git/cOnFiG")
189
except errors.NoSuchFile:
190
self.case_sensitive = True
192
self.case_sensitive = False
194
def merge_modified(self):
197
def set_merge_modified(self, modified_hashes):
198
raise errors.UnsupportedOperation(self.set_merge_modified, self)
200
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
201
self.set_parent_ids([p for p, t in parents_list])
203
def _set_merges_from_parent_ids(self, rhs_parent_ids):
205
merges = [self.branch.lookup_bzr_revision_id(revid)[0] for revid in rhs_parent_ids]
206
except errors.NoSuchRevision as e:
207
raise errors.GhostRevisionUnusableHere(e.revision)
209
self.control_transport.put_bytes('MERGE_HEAD', '\n'.join(merges),
210
mode=self.controldir._get_file_mode())
213
self.control_transport.delete('MERGE_HEAD')
214
except errors.NoSuchFile:
217
def set_parent_ids(self, revision_ids, allow_leftmost_as_ghost=False):
218
"""Set the parent ids to revision_ids.
220
See also set_parent_trees. This api will try to retrieve the tree data
221
for each element of revision_ids from the trees repository. If you have
222
tree data already available, it is more efficient to use
223
set_parent_trees rather than set_parent_ids. set_parent_ids is however
224
an easier API to use.
226
:param revision_ids: The revision_ids to set as the parent ids of this
227
working tree. Any of these may be ghosts.
229
with self.lock_tree_write():
230
self._check_parents_for_ghosts(revision_ids,
231
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
232
for revision_id in revision_ids:
233
_mod_revision.check_not_reserved_id(revision_id)
235
revision_ids = self._filter_parent_ids_by_ancestry(revision_ids)
237
if len(revision_ids) > 0:
238
self.set_last_revision(revision_ids[0])
240
self.set_last_revision(_mod_revision.NULL_REVISION)
242
self._set_merges_from_parent_ids(revision_ids[1:])
244
def get_parent_ids(self):
245
"""See Tree.get_parent_ids.
247
This implementation reads the pending merges list and last_revision
248
value and uses that to decide what the parents list should be.
250
last_rev = _mod_revision.ensure_null(self._last_revision())
251
if _mod_revision.NULL_REVISION == last_rev:
256
merges_bytes = self.control_transport.get_bytes('MERGE_HEAD')
257
except errors.NoSuchFile:
260
for l in osutils.split_lines(merges_bytes):
261
revision_id = l.rstrip('\n')
262
parents.append(self.branch.lookup_foreign_revision_id(revision_id))
265
def iter_children(self, file_id):
266
dpath = self.id2path(file_id) + "/"
267
if dpath in self.index:
269
for path in self.index:
270
if not path.startswith(dpath):
272
if "/" in path[len(dpath):]:
273
# Not a direct child but something further down
275
yield self.path2id(path)
277
def check_state(self):
278
"""Check that the working state is/isn't valid."""
281
def remove(self, files, verbose=False, to_file=None, keep_files=True,
283
"""Remove nominated files from the working tree metadata.
285
:param files: File paths relative to the basedir.
286
:param keep_files: If true, the files will also be kept.
287
:param force: Delete files and directories, even if they are changed
288
and even if the directories are not empty.
290
if isinstance(files, basestring):
296
def backup(file_to_backup):
297
abs_path = self.abspath(file_to_backup)
298
backup_name = self.controldir._available_backup_name(file_to_backup)
299
osutils.rename(abs_path, self.abspath(backup_name))
300
return "removed %s (but kept a copy: %s)" % (
301
file_to_backup, backup_name)
303
# Sort needed to first handle directory content before the directory
308
def recurse_directory_to_add_files(directory):
309
# Recurse directory and add all files
310
# so we can check if they have changed.
311
for parent_info, file_infos in self.walkdirs(directory):
312
for relpath, basename, kind, lstat, fileid, kind in file_infos:
313
# Is it versioned or ignored?
314
if self.is_versioned(relpath):
315
# Add nested content for deletion.
316
all_files.add(relpath)
318
# Files which are not versioned
319
# should be treated as unknown.
320
files_to_backup.append(relpath)
322
with self.lock_tree_write():
323
for filepath in files:
324
# Get file name into canonical form.
325
abspath = self.abspath(filepath)
326
filepath = self.relpath(abspath)
329
all_files.add(filepath)
330
recurse_directory_to_add_files(filepath)
332
files = list(all_files)
335
return # nothing to do
337
# Sort needed to first handle directory content before the directory
338
files.sort(reverse=True)
340
# Bail out if we are going to delete files we shouldn't
341
if not keep_files and not force:
342
for (file_id, path, content_change, versioned, parent_id, name,
343
kind, executable) in self.iter_changes(self.basis_tree(),
344
include_unchanged=True, require_versioned=False,
345
want_unversioned=True, specific_files=files):
346
if versioned[0] == False:
347
# The record is unknown or newly added
348
files_to_backup.append(path[1])
349
files_to_backup.extend(osutils.parent_directories(path[1]))
350
elif (content_change and (kind[1] is not None) and
351
osutils.is_inside_any(files, path[1])):
352
# Versioned and changed, but not deleted, and still
353
# in one of the dirs to be deleted.
354
files_to_backup.append(path[1])
355
files_to_backup.extend(osutils.parent_directories(path[1]))
363
except errors.NoSuchFile:
366
abs_path = self.abspath(f)
368
# having removed it, it must be either ignored or unknown
369
if self.is_ignored(f):
373
kind_ch = osutils.kind_marker(kind)
374
to_file.write(new_status + ' ' + f + kind_ch + '\n')
376
message = "%s does not exist" % (f, )
379
if f in files_to_backup and not force:
382
if kind == 'directory':
383
osutils.rmtree(abs_path)
385
osutils.delete_any(abs_path)
386
message = "deleted %s" % (f,)
388
message = "removed %s" % (f,)
389
self._unversion_path(f)
391
# print only one message (if any) per file.
392
if message is not None:
394
self._versioned_dirs = None
397
def smart_add(self, file_list, recurse=True, action=None, save=True):
401
# expand any symlinks in the directory part, while leaving the
403
# only expanding if symlinks are supported avoids windows path bugs
404
if osutils.has_symlinks():
405
file_list = list(map(osutils.normalizepath, file_list))
407
conflicts_related = set()
408
for c in self.conflicts():
409
conflicts_related.update(c.associated_filenames())
414
def call_action(filepath, kind):
415
if action is not None:
416
parent_path = posixpath.dirname(filepath)
417
parent_id = self.path2id(parent_path)
418
parent_ie = self._get_dir_ie(parent_path, parent_id)
419
file_id = action(self, parent_ie, filepath, kind)
420
if file_id is not None:
421
raise workingtree.SettingFileIdUnsupported()
423
with self.lock_tree_write():
424
for filepath in osutils.canonical_relpaths(self.basedir, file_list):
425
filepath, can_access = osutils.normalized_filename(filepath)
427
raise errors.InvalidNormalization(filepath)
429
abspath = self.abspath(filepath)
430
kind = osutils.file_kind(abspath)
431
if kind in ("file", "symlink"):
432
if filepath in self.index:
435
call_action(filepath, kind)
437
self._index_add_entry(filepath, kind)
438
added.append(filepath)
439
elif kind == "directory":
440
if filepath not in self.index:
441
call_action(filepath, kind)
443
user_dirs.append(filepath)
445
raise errors.BadFileKindError(filename=abspath, kind=kind)
446
for user_dir in user_dirs:
447
abs_user_dir = self.abspath(user_dir)
450
transport = _mod_transport.get_transport_from_path(abs_user_dir)
451
_mod_controldir.ControlDirFormat.find_format(transport)
453
except errors.NotBranchError:
455
except errors.UnsupportedFormatError:
460
trace.warning('skipping nested tree %r', abs_user_dir)
463
for name in os.listdir(abs_user_dir):
464
subp = os.path.join(user_dir, name)
465
if self.is_control_filename(subp) or self.mapping.is_special_file(subp):
467
ignore_glob = self.is_ignored(subp)
468
if ignore_glob is not None:
469
ignored.setdefault(ignore_glob, []).append(subp)
471
abspath = self.abspath(subp)
472
kind = osutils.file_kind(abspath)
473
if kind == "directory":
474
user_dirs.append(subp)
476
if subp in self.index:
479
if subp in conflicts_related:
481
call_action(filepath, kind)
483
self._index_add_entry(subp, kind)
487
return added, ignored
489
def has_filename(self, filename):
490
return osutils.lexists(self.abspath(filename))
492
def _iter_files_recursive(self, from_dir=None, include_dirs=False):
495
for (dirpath, dirnames, filenames) in os.walk(self.abspath(from_dir).encode(osutils._fs_enc)):
496
dir_relpath = dirpath[len(self.basedir):].strip("/")
497
if self.controldir.is_control_filename(dir_relpath):
499
for name in list(dirnames):
500
if self.controldir.is_control_filename(name):
501
dirnames.remove(name)
503
relpath = os.path.join(dir_relpath, name)
506
yield relpath.decode(osutils._fs_enc)
507
except UnicodeDecodeError:
508
raise errors.BadFilenameEncoding(
509
relpath, osutils._fs_enc)
510
if not self._has_dir(relpath):
511
dirnames.remove(name)
512
for name in filenames:
513
if not self.mapping.is_special_file(name):
514
yp = os.path.join(dir_relpath, name)
516
yield yp.decode(osutils._fs_enc)
517
except UnicodeDecodeError:
518
raise errors.BadFilenameEncoding(
522
"""Yield all unversioned files in this WorkingTree.
524
with self.lock_read():
525
for p in (set(self._iter_files_recursive(include_dirs=True)) - set([p.decode('utf-8') for p in self.index])):
526
if not self._has_dir(p):
530
# TODO: Maybe this should only write on dirty ?
531
if self._lock_mode != 'w':
532
raise errors.NotWriteLocked(self)
535
def has_or_had_id(self, file_id):
536
if self.has_id(file_id):
538
if self.had_id(file_id):
542
def had_id(self, file_id):
543
path = self._basis_fileid_map.lookup_file_id(file_id)
545
head = self.repository._git.head()
547
# Assume no if basis is not accessible
550
root_tree = self.store[head].tree
554
tree_lookup_path(self.store.__getitem__, root_tree, path)
560
def get_file_mtime(self, path, file_id=None):
561
"""See Tree.get_file_mtime."""
563
return self._lstat(path).st_mtime
564
except OSError, (num, msg):
565
if num == errno.ENOENT:
566
raise errors.NoSuchFile(path)
569
def is_ignored(self, filename):
570
r"""Check whether the filename matches an ignore pattern.
572
If the file is ignored, returns the pattern which caused it to
573
be ignored, otherwise None. So this can simply be used as a
574
boolean if desired."""
575
if getattr(self, '_global_ignoreglobster', None) is None:
577
ignore_globs.update(ignores.get_runtime_ignores())
578
ignore_globs.update(ignores.get_user_ignores())
579
self._global_ignoreglobster = globbing.ExceptionGlobster(ignore_globs)
580
match = self._global_ignoreglobster.match(filename)
581
if match is not None:
584
if self.kind(filename) == 'directory':
586
except errors.NoSuchFile:
588
filename = filename.lstrip(b'/')
589
ignore_manager = self._get_ignore_manager()
590
ps = list(ignore_manager.find_matching(filename))
593
if not ps[-1].is_exclude:
597
def _get_ignore_manager(self):
598
ignoremanager = getattr(self, '_ignoremanager', None)
599
if ignoremanager is not None:
602
ignore_manager = IgnoreFilterManager.from_repo(self.repository._git)
603
self._ignoremanager = ignore_manager
604
return ignore_manager
606
def _flush_ignore_list_cache(self):
607
self._ignoremanager = None
609
def set_last_revision(self, revid):
610
if _mod_revision.is_null(revid):
611
self.branch.set_last_revision_info(0, revid)
613
_mod_revision.check_not_reserved_id(revid)
615
self.branch.generate_revision_history(revid)
616
except errors.NoSuchRevision:
617
raise errors.GhostRevisionUnusableHere(revid)
619
def _reset_data(self):
621
head = self.repository._git.head()
623
self._basis_fileid_map = GitFileIdMap({}, self.mapping)
625
self._basis_fileid_map = self.mapping.get_fileid_map(
626
self.store.__getitem__, self.store[head].tree)
627
self._fileid_map = self._basis_fileid_map.copy()
629
def get_file_verifier(self, path, file_id=None, stat_value=None):
630
with self.lock_read():
632
return ("GIT", self.index[path.encode('utf-8')].sha)
634
if self._has_dir(path):
636
raise errors.NoSuchFile(path)
638
def get_file_sha1(self, path, file_id=None, stat_value=None):
639
with self.lock_read():
640
if not self.is_versioned(path):
641
raise errors.NoSuchFile(path)
642
abspath = self.abspath(path)
644
return osutils.sha_file_by_name(abspath)
645
except OSError, (num, msg):
646
if num in (errno.EISDIR, errno.ENOENT):
650
def revision_tree(self, revid):
651
return self.repository.revision_tree(revid)
653
def filter_unversioned_files(self, files):
654
return set([p for p in files if not self.is_versioned(p)])
656
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
657
mode = stat_result.st_mode
658
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
660
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
661
return self.basis_tree().is_executable(path)
663
def stored_kind(self, path, file_id=None):
664
with self.lock_read():
666
return mode_kind(self.index[path.encode("utf-8")].mode)
668
# Maybe it's a directory?
669
if self._has_dir(path):
671
raise errors.NoSuchFile(path)
673
def _lstat(self, path):
674
return os.lstat(self.abspath(path))
676
def is_executable(self, path, file_id=None):
677
with self.lock_read():
678
if getattr(self, "_supports_executable", osutils.supports_executable)():
679
mode = self._lstat(path).st_mode
682
mode = self.index[path.encode('utf-8')].mode
685
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
687
def _is_executable_from_path_and_stat(self, path, stat_result):
688
if getattr(self, "_supports_executable", osutils.supports_executable)():
689
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
691
return self._is_executable_from_path_and_stat_from_basis(path, stat_result)
693
def list_files(self, include_root=False, from_dir=None, recursive=True):
697
fk_entries = {'directory': tree.TreeDirectory,
698
'file': tree.TreeFile,
699
'symlink': tree.TreeLink}
700
with self.lock_read():
701
root_ie = self._get_dir_ie(u"", None)
702
if include_root and not from_dir:
703
yield "", "V", root_ie.kind, root_ie.file_id, root_ie
704
dir_ids[u""] = root_ie.file_id
706
path_iterator = sorted(self._iter_files_recursive(from_dir, include_dirs=True))
708
path_iterator = sorted([os.path.join(from_dir, name.decode(osutils._fs_enc)) for name in
709
os.listdir(self.abspath(from_dir).encode(osutils._fs_enc)) if not self.controldir.is_control_filename(name)
710
and not self.mapping.is_special_file(name)])
711
for path in path_iterator:
713
index_path = path.encode("utf-8")
714
except UnicodeEncodeError:
715
raise errors.BadFilenameEncoding(
716
path, osutils._fs_enc)
718
value = self.index[index_path]
721
kind = osutils.file_kind(self.abspath(path))
722
parent, name = posixpath.split(path)
723
for dir_path, dir_ie in self._add_missing_parent_ids(parent, dir_ids):
725
if kind == 'directory':
727
if self._has_dir(path):
728
ie = self._get_dir_ie(path, self.path2id(path))
731
elif self.is_ignored(path):
733
ie = fk_entries[kind]()
737
ie = fk_entries[kind]()
739
yield posixpath.relpath(path, from_dir), status, kind, file_id, ie
741
if value is not None:
742
ie = self._get_file_ie(name, path, value, dir_ids[parent])
743
yield posixpath.relpath(path, from_dir), "V", ie.kind, ie.file_id, ie
745
ie = fk_entries[kind]()
746
yield posixpath.relpath(path, from_dir), ("I" if self.is_ignored(path) else "?"), kind, None, ie
748
def all_file_ids(self):
749
with self.lock_read():
750
ids = {u"": self.path2id("")}
751
for path in self.index:
752
if self.mapping.is_special_file(path):
754
path = path.decode("utf-8")
755
parent = posixpath.dirname(path).strip("/")
756
for e in self._add_missing_parent_ids(parent, ids):
758
ids[path] = self.path2id(path)
759
return set(ids.values())
761
def all_versioned_paths(self):
762
with self.lock_read():
764
for path in self.index:
765
if self.mapping.is_special_file(path):
767
path = path.decode("utf-8")
770
path = posixpath.dirname(path).strip("/")
776
def _directory_is_tree_reference(self, path):
777
# FIXME: Check .gitsubmodules for path
780
def iter_child_entries(self, path, file_id=None):
781
encoded_path = path.encode('utf-8')
782
with self.lock_read():
783
parent_id = self.path2id(path)
785
seen_children = set()
786
for item_path, value in self.index.iteritems():
787
if self.mapping.is_special_file(item_path):
789
if not osutils.is_inside(encoded_path, item_path):
792
subpath = posixpath.relpath(item_path, encoded_path)
794
dirname = subpath.split(b'/', 1)[0]
795
file_ie = self._get_dir_ie(posixpath.join(path, dirname), parent_id)
797
(parent, name) = posixpath.split(item_path)
798
file_ie = self._get_file_ie(
799
name.decode('utf-8'),
800
item_path.decode('utf-8'), value, parent_id)
802
if not found_any and path != u'':
803
raise errors.NoSuchFile(path)
806
with self.lock_read():
807
conflicts = _mod_conflicts.ConflictList()
808
for item_path, value in self.index.iteritems():
809
if value.flags & FLAG_STAGEMASK:
810
conflicts.append(_mod_conflicts.TextConflict(item_path.decode('utf-8')))
813
def set_conflicts(self, conflicts):
815
for conflict in conflicts:
816
if conflict.typestring in ('text conflict', 'contents conflict'):
817
by_path.add(conflict.path.encode('utf-8'))
819
raise errors.UnsupportedOperation(self.set_conflicts, self)
820
with self.lock_tree_write():
821
for path in self.index:
822
self._set_conflicted(path, path in by_path)
825
def _set_conflicted(self, path, conflicted):
826
trace.mutter('change conflict: %r -> %r', path, conflicted)
827
value = self.index[path]
829
self.index[path] = (value[:9] + (value[9] | FLAG_STAGEMASK, ))
831
self.index[path] = (value[:9] + (value[9] &~ FLAG_STAGEMASK, ))
833
def add_conflicts(self, new_conflicts):
834
with self.lock_tree_write():
835
for conflict in new_conflicts:
836
if conflict.typestring in ('text conflict', 'contents conflict'):
838
self._set_conflicted(conflict.path.encode('utf-8'), True)
840
raise errors.UnsupportedOperation(self.add_conflicts, self)
842
raise errors.UnsupportedOperation(self.add_conflicts, self)
845
def walkdirs(self, prefix=""):
846
"""Walk the directories of this tree.
848
returns a generator which yields items in the form:
849
((curren_directory_path, fileid),
850
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
853
This API returns a generator, which is only valid during the current
854
tree transaction - within a single lock_read or lock_write duration.
856
If the tree is not locked, it may cause an error to be raised,
857
depending on the tree implementation.
859
from bisect import bisect_left
861
disk_top = self.abspath(prefix)
862
if disk_top.endswith('/'):
863
disk_top = disk_top[:-1]
864
top_strip_len = len(disk_top) + 1
865
inventory_iterator = self._walkdirs(prefix)
866
disk_iterator = osutils.walkdirs(disk_top, prefix)
868
current_disk = next(disk_iterator)
869
disk_finished = False
871
if not (e.errno == errno.ENOENT or
872
(sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
877
current_inv = next(inventory_iterator)
879
except StopIteration:
882
while not inv_finished or not disk_finished:
884
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
885
cur_disk_dir_content) = current_disk
887
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
888
cur_disk_dir_content) = ((None, None), None)
889
if not disk_finished:
890
# strip out .bzr dirs
891
if (cur_disk_dir_path_from_top[top_strip_len:] == '' and
892
len(cur_disk_dir_content) > 0):
893
# osutils.walkdirs can be made nicer -
894
# yield the path-from-prefix rather than the pathjoined
896
bzrdir_loc = bisect_left(cur_disk_dir_content,
898
if (bzrdir_loc < len(cur_disk_dir_content)
899
and self.controldir.is_control_filename(
900
cur_disk_dir_content[bzrdir_loc][0])):
901
# we dont yield the contents of, or, .bzr itself.
902
del cur_disk_dir_content[bzrdir_loc]
904
# everything is unknown
907
# everything is missing
910
direction = cmp(current_inv[0][0], cur_disk_dir_relpath)
912
# disk is before inventory - unknown
913
dirblock = [(relpath, basename, kind, stat, None, None) for
914
relpath, basename, kind, stat, top_path in
915
cur_disk_dir_content]
916
yield (cur_disk_dir_relpath, None), dirblock
918
current_disk = next(disk_iterator)
919
except StopIteration:
922
# inventory is before disk - missing.
923
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
924
for relpath, basename, dkind, stat, fileid, kind in
926
yield (current_inv[0][0], current_inv[0][1]), dirblock
928
current_inv = next(inventory_iterator)
929
except StopIteration:
932
# versioned present directory
933
# merge the inventory and disk data together
935
for relpath, subiterator in itertools.groupby(sorted(
936
current_inv[1] + cur_disk_dir_content,
937
key=operator.itemgetter(0)), operator.itemgetter(1)):
938
path_elements = list(subiterator)
939
if len(path_elements) == 2:
940
inv_row, disk_row = path_elements
941
# versioned, present file
942
dirblock.append((inv_row[0],
943
inv_row[1], disk_row[2],
944
disk_row[3], inv_row[4],
946
elif len(path_elements[0]) == 5:
948
dirblock.append((path_elements[0][0],
949
path_elements[0][1], path_elements[0][2],
950
path_elements[0][3], None, None))
951
elif len(path_elements[0]) == 6:
952
# versioned, absent file.
953
dirblock.append((path_elements[0][0],
954
path_elements[0][1], 'unknown', None,
955
path_elements[0][4], path_elements[0][5]))
957
raise NotImplementedError('unreachable code')
958
yield current_inv[0], dirblock
960
current_inv = next(inventory_iterator)
961
except StopIteration:
964
current_disk = next(disk_iterator)
965
except StopIteration:
968
def _walkdirs(self, prefix=""):
971
prefix = prefix.encode('utf-8')
972
per_dir = defaultdict(set)
974
per_dir[('', self.get_root_id())] = set()
975
def add_entry(path, kind):
976
if path == '' or not path.startswith(prefix):
978
(dirname, child_name) = posixpath.split(path)
979
add_entry(dirname, 'directory')
980
dirname = dirname.decode("utf-8")
981
dir_file_id = self.path2id(dirname)
982
if not isinstance(value, tuple) or len(value) != 10:
983
raise ValueError(value)
984
per_dir[(dirname, dir_file_id)].add(
985
(path.decode("utf-8"), child_name.decode("utf-8"),
987
self.path2id(path.decode("utf-8")),
989
with self.lock_read():
990
for path, value in self.index.iteritems():
991
if self.mapping.is_special_file(path):
993
if not path.startswith(prefix):
995
add_entry(path, mode_kind(value.mode))
996
return ((k, sorted(v)) for (k, v) in sorted(per_dir.iteritems()))
998
def get_shelf_manager(self):
999
raise workingtree.ShelvingUnsupported()
1001
def store_uncommitted(self):
1002
raise errors.StoringUncommittedNotSupported(self)
1004
def apply_inventory_delta(self, changes):
1005
for (old_path, new_path, file_id, ie) in changes:
1006
if old_path is not None:
1007
del self.index[old_path.encode('utf-8')]
1008
self._versioned_dirs = None
1009
if new_path is not None and ie.kind != 'directory':
1010
self._index_add_entry(new_path, ie.kind)
1013
def annotate_iter(self, path, file_id=None,
1014
default_revision=_mod_revision.CURRENT_REVISION):
1015
"""See Tree.annotate_iter
1017
This implementation will use the basis tree implementation if possible.
1018
Lines not in the basis are attributed to CURRENT_REVISION
1020
If there are pending merges, lines added by those merges will be
1021
incorrectly attributed to CURRENT_REVISION (but after committing, the
1022
attribution will be correct).
1024
with self.lock_read():
1025
maybe_file_parent_keys = []
1026
for parent_id in self.get_parent_ids():
1028
parent_tree = self.revision_tree(parent_id)
1029
except errors.NoSuchRevisionInTree:
1030
parent_tree = self.branch.repository.revision_tree(
1032
with parent_tree.lock_read():
1033
# TODO(jelmer): Use rename/copy tracker to find path name in parent
1036
kind = parent_tree.kind(parent_path)
1037
except errors.NoSuchFile:
1040
# Note: this is slightly unnecessary, because symlinks and
1041
# directories have a "text" which is the empty text, and we
1042
# know that won't mess up annotations. But it seems cleaner
1046
parent_tree.get_file_revision(parent_path))
1047
if parent_text_key not in maybe_file_parent_keys:
1048
maybe_file_parent_keys.append(parent_text_key)
1049
graph = self.branch.repository.get_file_graph()
1050
heads = graph.heads(maybe_file_parent_keys)
1051
file_parent_keys = []
1052
for key in maybe_file_parent_keys:
1054
file_parent_keys.append(key)
1056
# Now we have the parents of this content
1057
from breezy.annotate import Annotator
1058
from .annotate import AnnotateProvider
1059
annotator = Annotator(AnnotateProvider(
1060
self.branch.repository._file_change_scanner))
1061
text = self.get_file_text(path)
1062
this_key = (path, default_revision)
1063
annotator.add_special_text(this_key, file_parent_keys, text)
1064
annotations = [(key[-1], line)
1065
for key, line in annotator.annotate_flat(this_key)]
1068
def _rename_one(self, from_rel, to_rel):
1069
os.rename(self.abspath(from_rel), self.abspath(to_rel))
1071
def _build_checkout_with_index(self):
1072
build_index_from_tree(
1073
self.user_transport.local_abspath('.'),
1074
self.control_transport.local_abspath("index"),
1076
None if self.branch.head is None else self.store[self.branch.head].tree)
1078
def reset_state(self, revision_ids=None):
1079
"""Reset the state of the working tree.
1081
This does a hard-reset to a last-known-good state. This is a way to
1082
fix if something got corrupted (like the .git/index file)
1084
with self.lock_tree_write():
1085
if revision_ids is not None:
1086
self.set_parent_ids(revision_ids)
1088
if self.branch.head is not None:
1089
for entry in self.store.iter_tree_contents(self.store[self.branch.head].tree):
1090
if not validate_path(entry.path):
1093
if S_ISGITLINK(entry.mode):
1094
pass # TODO(jelmer): record and return submodule paths
1096
# Let's at least try to use the working tree file:
1098
st = self._lstat(self.abspath(entry.path))
1099
except OSError, (num, msg):
1100
# But if it doesn't exist, we'll make something up.
1101
obj = self.store[entry.sha]
1102
st = os.stat_result((entry.mode, 0, 0, 0,
1103
0, 0, len(obj.as_raw_string()), 0,
1105
self.index[entry.path] = index_entry_from_stat(st, entry.sha, 0)
1108
def pull(self, source, overwrite=False, stop_revision=None,
1109
change_reporter=None, possible_transports=None, local=False,
1111
with self.lock_write(), source.lock_read():
1112
old_revision = self.branch.last_revision()
1113
basis_tree = self.basis_tree()
1114
count = self.branch.pull(source, overwrite, stop_revision,
1115
possible_transports=possible_transports,
1117
new_revision = self.branch.last_revision()
1118
if new_revision != old_revision:
1119
with basis_tree.lock_read():
1120
new_basis_tree = self.branch.basis_tree()
1126
change_reporter=change_reporter,
1127
show_base=show_base)
1131
class GitWorkingTreeFormat(workingtree.WorkingTreeFormat):
1133
_tree_class = GitWorkingTree
1135
supports_versioned_directories = False
1137
supports_setting_file_ids = False
1139
supports_store_uncommitted = False
1141
supports_leftmost_parent_id_as_ghost = False
1143
supports_righthand_parent_id_as_ghost = False
1145
requires_normalized_unicode_filenames = True
1147
supports_merge_modified = False
1150
def _matchingcontroldir(self):
1151
from .dir import LocalGitControlDirFormat
1152
return LocalGitControlDirFormat()
1154
def get_format_description(self):
1155
return "Git Working Tree"
1157
def initialize(self, a_controldir, revision_id=None, from_branch=None,
1158
accelerator_tree=None, hardlink=False):
1159
"""See WorkingTreeFormat.initialize()."""
1160
if not isinstance(a_controldir, LocalGitDir):
1161
raise errors.IncompatibleFormat(self, a_controldir)
1162
index = Index(a_controldir.root_transport.local_abspath(".git/index"))
1164
branch = a_controldir.open_branch()
1165
if revision_id is not None:
1166
branch.set_last_revision(revision_id)
1167
wt = GitWorkingTree(
1168
a_controldir, a_controldir.open_repository(), branch, index)
1169
for hook in MutableTree.hooks['post_build_tree']:
1174
class InterIndexGitTree(InterGitTrees):
1175
"""InterTree that works between a Git revision tree and an index."""
1177
def __init__(self, source, target):
1178
super(InterIndexGitTree, self).__init__(source, target)
1179
self._index = target.index
1182
def is_compatible(cls, source, target):
1183
from .repository import GitRevisionTree
1184
return (isinstance(source, GitRevisionTree) and
1185
isinstance(target, GitWorkingTree))
1187
def _iter_git_changes(self, want_unchanged=False, specific_files=None,
1188
require_versioned=False, include_root=False):
1189
if require_versioned and specific_files is not None:
1190
for path in specific_files:
1191
if (not self.source.is_versioned(path) and
1192
not self.target.is_versioned(path)):
1193
raise errors.PathsNotVersionedError(path)
1194
# TODO(jelmer): Restrict to specific_files, for performance reasons.
1195
with self.lock_read():
1196
return changes_between_git_tree_and_working_copy(
1197
self.source.store, self.source.tree,
1198
self.target, want_unchanged=want_unchanged,
1199
include_root=include_root)
1201
def compare(self, want_unchanged=False, specific_files=None,
1202
extra_trees=None, require_versioned=False, include_root=False,
1203
want_unversioned=False):
1204
with self.lock_read():
1205
changes = self._iter_git_changes(
1206
want_unchanged=want_unchanged,
1207
specific_files=specific_files,
1208
require_versioned=require_versioned,
1209
include_root=include_root)
1210
source_fileid_map = self.source._fileid_map
1211
target_fileid_map = self.target._fileid_map
1212
ret = tree_delta_from_git_changes(changes, self.target.mapping,
1213
(source_fileid_map, target_fileid_map),
1214
specific_files=specific_files, require_versioned=require_versioned,
1215
include_root=include_root)
1216
if want_unversioned:
1217
for e in self.target.extras():
1218
ret.unversioned.append(
1219
(osutils.normalized_filename(e)[0], None,
1220
osutils.file_kind(self.target.abspath(e))))
1223
def iter_changes(self, include_unchanged=False, specific_files=None,
1224
pb=None, extra_trees=[], require_versioned=True,
1225
want_unversioned=False):
1226
with self.lock_read():
1227
changes = self._iter_git_changes(
1228
want_unchanged=include_unchanged,
1229
specific_files=specific_files,
1230
require_versioned=require_versioned)
1231
if want_unversioned:
1232
changes = itertools.chain(
1234
untracked_changes(self.target))
1235
return changes_from_git_changes(
1236
changes, self.target.mapping,
1237
specific_files=specific_files,
1238
include_unchanged=include_unchanged)
1241
tree.InterTree.register_optimiser(InterIndexGitTree)
1244
def untracked_changes(tree):
1245
for e in tree.extras():
1246
ap = tree.abspath(e)
1249
np, accessible = osutils.normalized_filename(e)
1250
except UnicodeDecodeError:
1251
raise errors.BadFilenameEncoding(
1253
if stat.S_ISDIR(st.st_mode):
1256
obj_id = blob_from_path_and_stat(ap.encode('utf-8'), st).id
1257
yield ((None, np), (None, st.st_mode), (None, obj_id))
1260
def changes_between_git_tree_and_index(store, from_tree_sha, target,
1261
want_unchanged=False, update_index=False):
1262
"""Determine the changes between a git tree and a working tree with index.
1265
to_tree_sha = target.index.commit(store)
1266
return store.tree_changes(from_tree_sha, to_tree_sha, include_trees=True,
1267
want_unchanged=want_unchanged, change_type_same=True)
1270
def changes_between_git_tree_and_working_copy(store, from_tree_sha, target,
1271
want_unchanged=False, update_index=False, include_root=False):
1272
"""Determine the changes between a git tree and a working tree with index.
1275
blobs = iter_fresh_blobs(target.index, target.abspath('.').encode(sys.getfilesystemencoding()))
1276
to_tree_sha = commit_tree(store, blobs)
1277
return store.tree_changes(from_tree_sha, to_tree_sha, include_trees=True,
1278
want_unchanged=want_unchanged, change_type_same=True)