60
51
trees or versioned trees.
63
def changes_from(self, other, want_unchanged=False, specific_files=None,
64
extra_trees=None, require_versioned=False, include_root=False,
65
want_unversioned=False):
66
"""Return a TreeDelta of the changes from other to this tree.
68
:param other: A tree to compare with.
69
:param specific_files: An optional list of file paths to restrict the
70
comparison to. When mapping filenames to ids, all matches in all
71
trees (including optional extra_trees) are used, and all children of
72
matched directories are included.
73
:param want_unchanged: An optional boolean requesting the inclusion of
74
unchanged entries in the result.
75
:param extra_trees: An optional list of additional trees to use when
76
mapping the contents of specific_files (paths) to file_ids.
77
:param require_versioned: An optional boolean (defaults to False). When
78
supplied and True all the 'specific_files' must be versioned, or
79
a PathsNotVersionedError will be thrown.
80
:param want_unversioned: Scan for unversioned paths.
82
The comparison will be performed by an InterTree object looked up on
85
# Martin observes that Tree.changes_from returns a TreeDelta and this
86
# may confuse people, because the class name of the returned object is
87
# a synonym of the object referenced in the method name.
88
return InterTree.get(other, self).compare(
89
want_unchanged=want_unchanged,
90
specific_files=specific_files,
91
extra_trees=extra_trees,
92
require_versioned=require_versioned,
93
include_root=include_root,
94
want_unversioned=want_unversioned,
97
@symbol_versioning.deprecated_method(symbol_versioning.one_three)
98
def _iter_changes(self, *args, **kwargs):
99
return self.iter_changes(*args, **kwargs)
101
def iter_changes(self, from_tree, include_unchanged=False,
102
specific_files=None, pb=None, extra_trees=None,
103
require_versioned=True, want_unversioned=False):
104
intertree = InterTree.get(from_tree, self)
105
return intertree.iter_changes(include_unchanged, specific_files, pb,
106
extra_trees, require_versioned, want_unversioned=want_unversioned)
109
"""Get a list of the conflicts in the tree.
111
Each conflict is an instance of bzrlib.conflicts.Conflict.
113
return _mod_conflicts.ConflictList()
116
"""For trees that can have unversioned files, return all such paths."""
119
def get_parent_ids(self):
120
"""Get the parent ids for this tree.
122
:return: a list of parent ids. [] is returned to indicate
123
a tree with no parents.
124
:raises: BzrError if the parents are not known.
126
raise NotImplementedError(self.get_parent_ids)
128
54
def has_filename(self, filename):
129
55
"""True if the tree has given filename."""
130
raise NotImplementedError(self.has_filename)
56
raise NotImplementedError()
132
58
def has_id(self, file_id):
133
59
return self.inventory.has_id(file_id)
135
__contains__ = has_id
137
61
def has_or_had_id(self, file_id):
138
62
if file_id == self.inventory.root.file_id:
140
64
return self.inventory.has_id(file_id)
142
def is_ignored(self, filename):
143
"""Check whether the filename is ignored by this tree.
145
:param filename: The relative filename within the tree.
146
:return: True if the filename is ignored.
150
68
def __iter__(self):
151
69
return iter(self.inventory)
153
def all_file_ids(self):
154
"""Iterate through all file ids, including ids for missing files."""
155
return set(self.inventory)
157
71
def id2path(self, file_id):
158
"""Return the path for a file id.
162
72
return self.inventory.id2path(file_id)
164
def is_control_filename(self, filename):
165
"""True if filename is the name of a control file in this tree.
167
:param filename: A filename within the tree. This is a relative path
168
from the root of this tree.
170
This is true IF and ONLY IF the filename is part of the meta data
171
that bzr controls in this tree. I.E. a random .bzr directory placed
172
on disk will not be a control file for this tree.
174
return self.bzrdir.is_control_filename(filename)
177
def iter_entries_by_dir(self, specific_file_ids=None):
178
"""Walk the tree in 'by_dir' order.
180
This will yield each entry in the tree as a (path, entry) tuple.
181
The order that they are yielded is:
183
Directories are walked in a depth-first lexicographical order,
184
however, whenever a directory is reached, all of its direct child
185
nodes are yielded in lexicographical order before yielding the
188
For example, in the tree::
198
The yield order (ignoring root) would be::
199
a, f, a/b, a/d, a/b/c, a/d/e, f/g
201
return self.inventory.iter_entries_by_dir(
202
specific_file_ids=specific_file_ids)
204
def iter_references(self):
205
for path, entry in self.iter_entries_by_dir():
206
if entry.kind == 'tree-reference':
207
yield path, entry.file_id
209
def kind(self, file_id):
210
raise NotImplementedError("Tree subclass %s must implement kind"
211
% self.__class__.__name__)
213
def stored_kind(self, file_id):
214
"""File kind stored for this file_id.
216
May not match kind on disk for working trees. Always available
217
for versioned files, even when the file itself is missing.
219
return self.kind(file_id)
221
def path_content_summary(self, path):
222
"""Get a summary of the information about path.
224
:param path: A relative path within the tree.
225
:return: A tuple containing kind, size, exec, sha1-or-link.
226
Kind is always present (see tree.kind()).
227
size is present if kind is file, None otherwise.
228
exec is None unless kind is file and the platform supports the 'x'
230
sha1-or-link is the link target if kind is symlink, or the sha1 if
231
it can be obtained without reading the file.
233
raise NotImplementedError(self.path_content_summary)
235
def get_reference_revision(self, file_id, path=None):
236
raise NotImplementedError("Tree subclass %s must implement "
237
"get_reference_revision"
238
% self.__class__.__name__)
240
def _comparison_data(self, entry, path):
241
"""Return a tuple of kind, executable, stat_value for a file.
243
entry may be None if there is no inventory entry for the file, but
244
path must always be supplied.
246
kind is None if there is no file present (even if an inventory id is
247
present). executable is False for non-file entries.
249
raise NotImplementedError(self._comparison_data)
251
def _file_size(self, entry, stat_value):
252
raise NotImplementedError(self._file_size)
254
74
def _get_inventory(self):
255
75
return self._inventory
257
def get_file(self, file_id, path=None):
258
"""Return a file object for the file file_id in the tree.
260
If both file_id and path are defined, it is implementation defined as
261
to which one is used.
263
raise NotImplementedError(self.get_file)
265
def get_file_mtime(self, file_id, path=None):
266
"""Return the modification time for a file.
268
:param file_id: The handle for this file.
269
:param path: The path that this file can be found at.
270
These must point to the same object.
272
raise NotImplementedError(self.get_file_mtime)
274
def get_file_size(self, file_id):
275
"""Return the size of a file in bytes.
277
This applies only to regular files. If invoked on directories or
278
symlinks, it will return None.
279
:param file_id: The file-id of the file
281
raise NotImplementedError(self.get_file_size)
283
77
def get_file_by_path(self, path):
284
return self.get_file(self._inventory.path2id(path), path)
286
def iter_files_bytes(self, desired_files):
287
"""Iterate through file contents.
289
Files will not necessarily be returned in the order they occur in
290
desired_files. No specific order is guaranteed.
292
Yields pairs of identifier, bytes_iterator. identifier is an opaque
293
value supplied by the caller as part of desired_files. It should
294
uniquely identify the file version in the caller's context. (Examples:
295
an index number or a TreeTransform trans_id.)
297
bytes_iterator is an iterable of bytestrings for the file. The
298
kind of iterable and length of the bytestrings are unspecified, but for
299
this implementation, it is a tuple containing a single bytestring with
300
the complete text of the file.
302
:param desired_files: a list of (file_id, identifier) pairs
304
for file_id, identifier in desired_files:
305
# We wrap the string in a tuple so that we can return an iterable
306
# of bytestrings. (Technically, a bytestring is also an iterable
307
# of bytestrings, but iterating through each character is not
309
cur_file = (self.get_file_text(file_id),)
310
yield identifier, cur_file
312
def get_symlink_target(self, file_id):
313
"""Get the target for a given file_id.
315
It is assumed that the caller already knows that file_id is referencing
317
:param file_id: Handle for the symlink entry.
318
:return: The path the symlink points to.
320
raise NotImplementedError(self.get_symlink_target)
322
def get_root_id(self):
323
"""Return the file_id for the root of this tree."""
324
raise NotImplementedError(self.get_root_id)
326
def annotate_iter(self, file_id,
327
default_revision=_mod_revision.CURRENT_REVISION):
328
"""Return an iterator of revision_id, line tuples.
330
For working trees (and mutable trees in general), the special
331
revision_id 'current:' will be used for lines that are new in this
332
tree, e.g. uncommitted changes.
333
:param file_id: The file to produce an annotated version from
334
:param default_revision: For lines that don't match a basis, mark them
335
with this revision id. Not all implementations will make use of
338
raise NotImplementedError(self.annotate_iter)
340
def _get_plan_merge_data(self, file_id, other, base):
341
from bzrlib import merge, versionedfile
342
vf = versionedfile._PlanMergeVersionedFile(file_id)
343
last_revision_a = self._get_file_revision(file_id, vf, 'this:')
344
last_revision_b = other._get_file_revision(file_id, vf, 'other:')
346
last_revision_base = None
348
last_revision_base = base._get_file_revision(file_id, vf, 'base:')
349
return vf, last_revision_a, last_revision_b, last_revision_base
351
def plan_file_merge(self, file_id, other, base=None):
352
"""Generate a merge plan based on annotations.
354
If the file contains uncommitted changes in this tree, they will be
355
attributed to the 'current:' pseudo-revision. If the file contains
356
uncommitted changes in the other tree, they will be assigned to the
357
'other:' pseudo-revision.
359
data = self._get_plan_merge_data(file_id, other, base)
360
vf, last_revision_a, last_revision_b, last_revision_base = data
361
return vf.plan_merge(last_revision_a, last_revision_b,
364
def plan_file_lca_merge(self, file_id, other, base=None):
365
"""Generate a merge plan based lca-newness.
367
If the file contains uncommitted changes in this tree, they will be
368
attributed to the 'current:' pseudo-revision. If the file contains
369
uncommitted changes in the other tree, they will be assigned to the
370
'other:' pseudo-revision.
372
data = self._get_plan_merge_data(file_id, other, base)
373
vf, last_revision_a, last_revision_b, last_revision_base = data
374
return vf.plan_lca_merge(last_revision_a, last_revision_b,
377
def _iter_parent_trees(self):
378
"""Iterate through parent trees, defaulting to Tree.revision_tree."""
379
for revision_id in self.get_parent_ids():
381
yield self.revision_tree(revision_id)
382
except errors.NoSuchRevisionInTree:
383
yield self.repository.revision_tree(revision_id)
386
def _file_revision(revision_tree, file_id):
387
"""Determine the revision associated with a file in a given tree."""
388
revision_tree.lock_read()
390
return revision_tree.inventory[file_id].revision
392
revision_tree.unlock()
394
def _get_file_revision(self, file_id, vf, tree_revision):
395
"""Ensure that file_id, tree_revision is in vf to plan the merge."""
397
if getattr(self, '_repository', None) is None:
398
last_revision = tree_revision
399
parent_keys = [(file_id, self._file_revision(t, file_id)) for t in
400
self._iter_parent_trees()]
401
vf.add_lines((file_id, last_revision), parent_keys,
402
self.get_file(file_id).readlines())
403
repo = self.branch.repository
406
last_revision = self._file_revision(self, file_id)
407
base_vf = self._repository.texts
408
if base_vf not in vf.fallback_versionedfiles:
409
vf.fallback_versionedfiles.append(base_vf)
78
return self.get_file(self._inventory.path2id(path))
412
80
inventory = property(_get_inventory,
413
81
doc="Inventory of this Tree")
431
99
"file is actually %s" % fp['sha1'],
432
100
"store is probably damaged/corrupt"])
435
def path2id(self, path):
436
"""Return the id for path in this tree."""
437
return self._inventory.path2id(path)
439
def paths2ids(self, paths, trees=[], require_versioned=True):
440
"""Return all the ids that can be reached by walking from paths.
442
Each path is looked up in this tree and any extras provided in
443
trees, and this is repeated recursively: the children in an extra tree
444
of a directory that has been renamed under a provided path in this tree
445
are all returned, even if none exist under a provided path in this
446
tree, and vice versa.
448
:param paths: An iterable of paths to start converting to ids from.
449
Alternatively, if paths is None, no ids should be calculated and None
450
will be returned. This is offered to make calling the api unconditional
451
for code that *might* take a list of files.
452
:param trees: Additional trees to consider.
453
:param require_versioned: If False, do not raise NotVersionedError if
454
an element of paths is not versioned in this tree and all of trees.
456
return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
458
@symbol_versioning.deprecated_method(symbol_versioning.one_six)
459
103
def print_file(self, file_id):
460
104
"""Print file with id `file_id` to stdout."""
462
106
sys.stdout.write(self.get_file_text(file_id))
467
def revision_tree(self, revision_id):
468
"""Obtain a revision tree for the revision revision_id.
470
The intention of this method is to allow access to possibly cached
471
tree data. Implementors of this method should raise NoSuchRevision if
472
the tree is not locally available, even if they could obtain the
473
tree via a repository or some other means. Callers are responsible
474
for finding the ultimate source for a revision tree.
476
:param revision_id: The revision_id of the requested tree.
478
:raises: NoSuchRevision if the tree cannot be obtained.
480
raise errors.NoSuchRevisionInTree(self, revision_id)
483
"""What files are present in this tree and unknown.
485
:return: an iterator over the unknown files.
492
def filter_unversioned_files(self, paths):
493
"""Filter out paths that are versioned.
495
:return: set of paths.
497
# NB: we specifically *don't* call self.has_filename, because for
498
# WorkingTrees that can indicate files that exist on disk but that
500
pred = self.inventory.has_filename
501
return set((p for p in paths if not pred(p)))
503
def walkdirs(self, prefix=""):
504
"""Walk the contents of this tree from path down.
506
This yields all the data about the contents of a directory at a time.
507
After each directory has been yielded, if the caller has mutated the
508
list to exclude some directories, they are then not descended into.
510
The data yielded is of the form:
511
((directory-relpath, directory-path-from-root, directory-fileid),
512
[(relpath, basename, kind, lstat, path_from_tree_root, file_id,
513
versioned_kind), ...]),
514
- directory-relpath is the containing dirs relpath from prefix
515
- directory-path-from-root is the containing dirs path from /
516
- directory-fileid is the id of the directory if it is versioned.
517
- relpath is the relative path within the subtree being walked.
518
- basename is the basename
519
- kind is the kind of the file now. If unknonwn then the file is not
520
present within the tree - but it may be recorded as versioned. See
522
- lstat is the stat data *if* the file was statted.
523
- path_from_tree_root is the path from the root of the tree.
524
- file_id is the file_id if the entry is versioned.
525
- versioned_kind is the kind of the file as last recorded in the
526
versioning system. If 'unknown' the file is not versioned.
527
One of 'kind' and 'versioned_kind' must not be 'unknown'.
529
:param prefix: Start walking from prefix within the tree rather than
530
at the root. This allows one to walk a subtree but get paths that are
531
relative to a tree rooted higher up.
532
:return: an iterator over the directory data.
534
raise NotImplementedError(self.walkdirs)
536
def iter_search_rules(self, path_names, pref_names=None,
537
_default_searcher=rules._per_user_searcher):
538
"""Find the preferences for filenames in a tree.
540
:param path_names: an iterable of paths to find attributes for.
541
Paths are given relative to the root of the tree.
542
:param pref_names: the list of preferences to lookup - None for all
543
:param _default_searcher: private parameter to assist testing - don't use
544
:return: an iterator of tuple sequences, one per path-name.
545
See _RulesSearcher.get_items for details on the tuple sequence.
547
searcher = self._get_rules_searcher(_default_searcher)
548
if searcher is not None:
549
if pref_names is not None:
550
for path in path_names:
551
yield searcher.get_selected_items(path, pref_names)
553
for path in path_names:
554
yield searcher.get_items(path)
557
def _get_rules_searcher(self, default_searcher):
558
"""Get the RulesSearcher for this tree given the default one."""
559
searcher = default_searcher
109
def export(self, dest, format='dir', root=None):
110
"""Export this tree."""
112
exporter = exporters[format]
114
from bzrlib.errors import BzrCommandError
115
raise BzrCommandError("export format %r not supported" % format)
116
exporter(self, dest, root)
120
class RevisionTree(Tree):
121
"""Tree viewing a previous revision.
123
File text can be retrieved from the text store.
125
TODO: Some kind of `__repr__` method, but a good one
126
probably means knowing the branch and revision number,
127
or at least passing a description to the constructor.
130
def __init__(self, weave_store, inv, revision_id):
131
self._weave_store = weave_store
132
self._inventory = inv
133
self._revision_id = revision_id
135
def get_weave(self, file_id):
136
# FIXME: RevisionTree should be given a branch
137
# not a store, or the store should know the branch.
138
import bzrlib.transactions as transactions
139
return self._weave_store.get_weave(file_id,
140
transactions.PassThroughTransaction())
143
def get_file_lines(self, file_id):
144
ie = self._inventory[file_id]
145
weave = self.get_weave(file_id)
146
return weave.get(ie.revision)
149
def get_file_text(self, file_id):
150
return ''.join(self.get_file_lines(file_id))
153
def get_file(self, file_id):
154
return StringIO(self.get_file_text(file_id))
156
def get_file_size(self, file_id):
157
return self._inventory[file_id].text_size
159
def get_file_sha1(self, file_id):
160
ie = self._inventory[file_id]
161
if ie.kind == "file":
164
def is_executable(self, file_id):
165
ie = self._inventory[file_id]
166
if ie.kind != "file":
168
return self._inventory[file_id].executable
170
def has_filename(self, filename):
171
return bool(self.inventory.path2id(filename))
173
def list_files(self):
174
# The only files returned by this are those from the version
175
for path, entry in self.inventory.iter_entries():
176
yield path, 'V', entry.kind, entry.file_id, entry
178
def get_symlink_target(self, file_id):
179
ie = self._inventory[file_id]
180
return ie.symlink_target;
182
def kind(self, file_id):
183
return self._inventory[file_id].kind
563
185
class EmptyTree(Tree):
565
186
def __init__(self):
566
self._inventory = Inventory(root_id=None)
567
symbol_versioning.warn('EmptyTree is deprecated as of bzr 0.9 please'
568
' use repository.revision_tree instead.',
569
DeprecationWarning, stacklevel=2)
571
def get_parent_ids(self):
187
self._inventory = Inventory()
574
189
def get_symlink_target(self, file_id):
654
268
yield (old_name, new_name)
657
def find_ids_across_trees(filenames, trees, require_versioned=True):
658
"""Find the ids corresponding to specified filenames.
660
All matches in all trees will be used, and all children of matched
661
directories will be used.
663
:param filenames: The filenames to find file_ids for (if None, returns
665
:param trees: The trees to find file_ids within
666
:param require_versioned: if true, all specified filenames must occur in
668
:return: a set of file ids for the specified filenames and their children.
672
specified_path_ids = _find_ids_across_trees(filenames, trees,
674
return _find_children_across_trees(specified_path_ids, trees)
677
def _find_ids_across_trees(filenames, trees, require_versioned):
678
"""Find the ids corresponding to specified filenames.
680
All matches in all trees will be used, but subdirectories are not scanned.
682
:param filenames: The filenames to find file_ids for
683
:param trees: The trees to find file_ids within
684
:param require_versioned: if true, all specified filenames must occur in
686
:return: a set of file ids for the specified filenames
689
interesting_ids = set()
690
for tree_path in filenames:
693
file_id = tree.path2id(tree_path)
694
if file_id is not None:
695
interesting_ids.add(file_id)
698
not_versioned.append(tree_path)
699
if len(not_versioned) > 0 and require_versioned:
700
raise errors.PathsNotVersionedError(not_versioned)
701
return interesting_ids
704
def _find_children_across_trees(specified_ids, trees):
705
"""Return a set including specified ids and their children.
707
All matches in all trees will be used.
709
:param trees: The trees to find file_ids within
710
:return: a set containing all specified ids and their children
712
interesting_ids = set(specified_ids)
713
pending = interesting_ids
714
# now handle children of interesting ids
715
# we loop so that we handle all children of each id in both trees
716
while len(pending) > 0:
718
for file_id in pending:
720
if not tree.has_id(file_id):
722
entry = tree.inventory[file_id]
723
for child in getattr(entry, 'children', {}).itervalues():
724
if child.file_id not in interesting_ids:
725
new_pending.add(child.file_id)
726
interesting_ids.update(new_pending)
727
pending = new_pending
728
return interesting_ids
731
class InterTree(InterObject):
732
"""This class represents operations taking place between two Trees.
734
Its instances have methods like 'compare' and contain references to the
735
source and target trees these operations are to be carried out on.
737
Clients of bzrlib should not need to use InterTree directly, rather they
738
should use the convenience methods on Tree such as 'Tree.compare()' which
739
will pass through to InterTree as appropriate.
745
def compare(self, want_unchanged=False, specific_files=None,
746
extra_trees=None, require_versioned=False, include_root=False,
747
want_unversioned=False):
748
"""Return the changes from source to target.
750
:return: A TreeDelta.
751
:param specific_files: An optional list of file paths to restrict the
752
comparison to. When mapping filenames to ids, all matches in all
753
trees (including optional extra_trees) are used, and all children of
754
matched directories are included.
755
:param want_unchanged: An optional boolean requesting the inclusion of
756
unchanged entries in the result.
757
:param extra_trees: An optional list of additional trees to use when
758
mapping the contents of specific_files (paths) to file_ids.
759
:param require_versioned: An optional boolean (defaults to False). When
760
supplied and True all the 'specific_files' must be versioned, or
761
a PathsNotVersionedError will be thrown.
762
:param want_unversioned: Scan for unversioned paths.
764
# NB: show_status depends on being able to pass in non-versioned files
765
# and report them as unknown
766
trees = (self.source,)
767
if extra_trees is not None:
768
trees = trees + tuple(extra_trees)
769
# target is usually the newer tree:
770
specific_file_ids = self.target.paths2ids(specific_files, trees,
771
require_versioned=require_versioned)
772
if specific_files and not specific_file_ids:
773
# All files are unversioned, so just return an empty delta
774
# _compare_trees would think we want a complete delta
775
result = delta.TreeDelta()
776
fake_entry = InventoryFile('unused', 'unused', 'unused')
777
result.unversioned = [(path, None,
778
self.target._comparison_data(fake_entry, path)[0]) for path in
781
return delta._compare_trees(self.source, self.target, want_unchanged,
782
specific_files, include_root, extra_trees=extra_trees,
783
require_versioned=require_versioned,
784
want_unversioned=want_unversioned)
786
def iter_changes(self, include_unchanged=False,
787
specific_files=None, pb=None, extra_trees=[],
788
require_versioned=True, want_unversioned=False):
789
"""Generate an iterator of changes between trees.
792
(file_id, (path_in_source, path_in_target),
793
changed_content, versioned, parent, name, kind,
796
Changed_content is True if the file's content has changed. This
797
includes changes to its kind, and to a symlink's target.
799
versioned, parent, name, kind, executable are tuples of (from, to).
800
If a file is missing in a tree, its kind is None.
802
Iteration is done in parent-to-child order, relative to the target
805
There is no guarantee that all paths are in sorted order: the
806
requirement to expand the search due to renames may result in children
807
that should be found early being found late in the search, after
808
lexically later results have been returned.
809
:param require_versioned: Raise errors.PathsNotVersionedError if a
810
path in the specific_files list is not versioned in one of
811
source, target or extra_trees.
812
:param want_unversioned: Should unversioned files be returned in the
813
output. An unversioned file is defined as one with (False, False)
814
for the versioned pair.
817
lookup_trees = [self.source]
819
lookup_trees.extend(extra_trees)
820
if specific_files == []:
821
specific_file_ids = []
823
specific_file_ids = self.target.paths2ids(specific_files,
824
lookup_trees, require_versioned=require_versioned)
826
all_unversioned = sorted([(p.split('/'), p) for p in
828
if specific_files is None or
829
osutils.is_inside_any(specific_files, p)])
830
all_unversioned = deque(all_unversioned)
832
all_unversioned = deque()
834
from_entries_by_dir = list(self.source.inventory.iter_entries_by_dir(
835
specific_file_ids=specific_file_ids))
836
from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
837
to_entries_by_dir = list(self.target.inventory.iter_entries_by_dir(
838
specific_file_ids=specific_file_ids))
839
num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
841
# the unversioned path lookup only occurs on real trees - where there
842
# can be extras. So the fake_entry is solely used to look up
843
# executable it values when execute is not supported.
844
fake_entry = InventoryFile('unused', 'unused', 'unused')
845
for to_path, to_entry in to_entries_by_dir:
846
while all_unversioned and all_unversioned[0][0] < to_path.split('/'):
847
unversioned_path = all_unversioned.popleft()
848
to_kind, to_executable, to_stat = \
849
self.target._comparison_data(fake_entry, unversioned_path[1])
850
yield (None, (None, unversioned_path[1]), True, (False, False),
852
(None, unversioned_path[0][-1]),
854
(None, to_executable))
855
file_id = to_entry.file_id
856
to_paths[file_id] = to_path
858
changed_content = False
859
from_path, from_entry = from_data.get(file_id, (None, None))
860
from_versioned = (from_entry is not None)
861
if from_entry is not None:
862
from_versioned = True
863
from_name = from_entry.name
864
from_parent = from_entry.parent_id
865
from_kind, from_executable, from_stat = \
866
self.source._comparison_data(from_entry, from_path)
869
from_versioned = False
873
from_executable = None
874
versioned = (from_versioned, True)
875
to_kind, to_executable, to_stat = \
876
self.target._comparison_data(to_entry, to_path)
877
kind = (from_kind, to_kind)
878
if kind[0] != kind[1]:
879
changed_content = True
880
elif from_kind == 'file':
881
from_size = self.source._file_size(from_entry, from_stat)
882
to_size = self.target._file_size(to_entry, to_stat)
883
if from_size != to_size:
884
changed_content = True
885
elif (self.source.get_file_sha1(file_id, from_path, from_stat) !=
886
self.target.get_file_sha1(file_id, to_path, to_stat)):
887
changed_content = True
888
elif from_kind == 'symlink':
889
if (self.source.get_symlink_target(file_id) !=
890
self.target.get_symlink_target(file_id)):
891
changed_content = True
892
elif from_kind == 'tree-reference':
893
if (self.source.get_reference_revision(file_id, from_path)
894
!= self.target.get_reference_revision(file_id, to_path)):
895
changed_content = True
896
parent = (from_parent, to_entry.parent_id)
897
name = (from_name, to_entry.name)
898
executable = (from_executable, to_executable)
900
pb.update('comparing files', entry_count, num_entries)
901
if (changed_content is not False or versioned[0] != versioned[1]
902
or parent[0] != parent[1] or name[0] != name[1] or
903
executable[0] != executable[1] or include_unchanged):
904
yield (file_id, (from_path, to_path), changed_content,
905
versioned, parent, name, kind, executable)
907
while all_unversioned:
908
# yield any trailing unversioned paths
909
unversioned_path = all_unversioned.popleft()
910
to_kind, to_executable, to_stat = \
911
self.target._comparison_data(fake_entry, unversioned_path[1])
912
yield (None, (None, unversioned_path[1]), True, (False, False),
914
(None, unversioned_path[0][-1]),
916
(None, to_executable))
918
def get_to_path(to_entry):
919
if to_entry.parent_id is None:
920
to_path = '' # the root
922
if to_entry.parent_id not in to_paths:
924
return get_to_path(self.target.inventory[to_entry.parent_id])
925
to_path = osutils.pathjoin(to_paths[to_entry.parent_id],
927
to_paths[to_entry.file_id] = to_path
930
for path, from_entry in from_entries_by_dir:
931
file_id = from_entry.file_id
932
if file_id in to_paths:
935
if not file_id in self.target.inventory:
936
# common case - paths we have not emitted are not present in
940
to_path = get_to_path(self.target.inventory[file_id])
943
pb.update('comparing files', entry_count, num_entries)
944
versioned = (True, False)
945
parent = (from_entry.parent_id, None)
946
name = (from_entry.name, None)
947
from_kind, from_executable, stat_value = \
948
self.source._comparison_data(from_entry, path)
949
kind = (from_kind, None)
950
executable = (from_executable, None)
951
changed_content = from_kind is not None
952
# the parent's path is necessarily known at this point.
953
yield(file_id, (path, to_path), changed_content, versioned, parent,
954
name, kind, executable)
957
class MultiWalker(object):
958
"""Walk multiple trees simultaneously, getting combined results."""
960
# Note: This could be written to not assume you can do out-of-order
961
# lookups. Instead any nodes that don't match in all trees could be
962
# marked as 'deferred', and then returned in the final cleanup loop.
963
# For now, I think it is "nicer" to return things as close to the
964
# "master_tree" order as we can.
966
def __init__(self, master_tree, other_trees):
967
"""Create a new MultiWalker.
969
All trees being walked must implement "iter_entries_by_dir()", such
970
that they yield (path, object) tuples, where that object will have a
971
'.file_id' member, that can be used to check equality.
973
:param master_tree: All trees will be 'slaved' to the master_tree such
974
that nodes in master_tree will be used as 'first-pass' sync points.
975
Any nodes that aren't in master_tree will be merged in a second
977
:param other_trees: A list of other trees to walk simultaneously.
979
self._master_tree = master_tree
980
self._other_trees = other_trees
982
# Keep track of any nodes that were properly processed just out of
983
# order, that way we don't return them at the end, we don't have to
984
# track *all* processed file_ids, just the out-of-order ones
985
self._out_of_order_processed = set()
988
def _step_one(iterator):
989
"""Step an iter_entries_by_dir iterator.
991
:return: (has_more, path, ie)
992
If has_more is False, path and ie will be None.
995
path, ie = iterator.next()
996
except StopIteration:
997
return False, None, None
999
return True, path, ie
1002
def _cmp_path_by_dirblock(path1, path2):
1003
"""Compare two paths based on what directory they are in.
1005
This generates a sort order, such that all children of a directory are
1006
sorted together, and grandchildren are in the same order as the
1007
children appear. But all grandchildren come after all children.
1009
:param path1: first path
1010
:param path2: the second path
1011
:return: negative number if ``path1`` comes first,
1012
0 if paths are equal
1013
and a positive number if ``path2`` sorts first
1015
# Shortcut this special case
1018
# This is stolen from _dirstate_helpers_py.py, only switching it to
1019
# Unicode objects. Consider using encode_utf8() and then using the
1020
# optimized versions, or maybe writing optimized unicode versions.
1021
if not isinstance(path1, unicode):
1022
raise TypeError("'path1' must be a unicode string, not %s: %r"
1023
% (type(path1), path1))
1024
if not isinstance(path2, unicode):
1025
raise TypeError("'path2' must be a unicode string, not %s: %r"
1026
% (type(path2), path2))
1027
return cmp(MultiWalker._path_to_key(path1),
1028
MultiWalker._path_to_key(path2))
1031
def _path_to_key(path):
1032
dirname, basename = osutils.split(path)
1033
return (dirname.split(u'/'), basename)
1035
def _lookup_by_file_id(self, extra_entries, other_tree, file_id):
1036
"""Lookup an inventory entry by file_id.
1038
This is called when an entry is missing in the normal order.
1039
Generally this is because a file was either renamed, or it was
1040
deleted/added. If the entry was found in the inventory and not in
1041
extra_entries, it will be added to self._out_of_order_processed
1043
:param extra_entries: A dictionary of {file_id: (path, ie)}. This
1044
should be filled with entries that were found before they were
1045
used. If file_id is present, it will be removed from the
1047
:param other_tree: The Tree to search, in case we didn't find the entry
1049
:param file_id: The file_id to look for
1050
:return: (path, ie) if found or (None, None) if not present.
1052
if file_id in extra_entries:
1053
return extra_entries.pop(file_id)
1054
# TODO: Is id2path better as the first call, or is
1055
# inventory[file_id] better as a first check?
1057
cur_path = other_tree.id2path(file_id)
1058
except errors.NoSuchId:
1060
if cur_path is None:
1063
self._out_of_order_processed.add(file_id)
1064
cur_ie = other_tree.inventory[file_id]
1065
return (cur_path, cur_ie)
1068
"""Match up the values in the different trees."""
1069
for result in self._walk_master_tree():
1071
self._finish_others()
1072
for result in self._walk_others():
1075
def _walk_master_tree(self):
1076
"""First pass, walk all trees in lock-step.
1078
When we are done, all nodes in the master_tree will have been
1079
processed. _other_walkers, _other_entries, and _others_extra will be
1080
set on 'self' for future processing.
1082
# This iterator has the most "inlining" done, because it tends to touch
1083
# every file in the tree, while the others only hit nodes that don't
1085
master_iterator = self._master_tree.iter_entries_by_dir()
1087
other_walkers = [other.iter_entries_by_dir()
1088
for other in self._other_trees]
1089
other_entries = [self._step_one(walker) for walker in other_walkers]
1090
# Track extra nodes in the other trees
1091
others_extra = [{} for i in xrange(len(self._other_trees))]
1093
master_has_more = True
1094
step_one = self._step_one
1095
lookup_by_file_id = self._lookup_by_file_id
1096
out_of_order_processed = self._out_of_order_processed
1098
while master_has_more:
1099
(master_has_more, path, master_ie) = step_one(master_iterator)
1100
if not master_has_more:
1103
file_id = master_ie.file_id
1105
other_values_append = other_values.append
1106
next_other_entries = []
1107
next_other_entries_append = next_other_entries.append
1108
for idx, (other_has_more, other_path, other_ie) in enumerate(other_entries):
1109
if not other_has_more:
1110
other_values_append(lookup_by_file_id(
1111
others_extra[idx], self._other_trees[idx], file_id))
1112
next_other_entries_append((False, None, None))
1113
elif file_id == other_ie.file_id:
1114
# This is the critical code path, as most of the entries
1115
# should match between most trees.
1116
other_values_append((other_path, other_ie))
1117
next_other_entries_append(step_one(other_walkers[idx]))
1119
# This walker did not match, step it until it either
1120
# matches, or we know we are past the current walker.
1121
other_walker = other_walkers[idx]
1122
other_extra = others_extra[idx]
1123
while (other_has_more and
1124
self._cmp_path_by_dirblock(other_path, path) < 0):
1125
other_file_id = other_ie.file_id
1126
if other_file_id not in out_of_order_processed:
1127
other_extra[other_file_id] = (other_path, other_ie)
1128
other_has_more, other_path, other_ie = \
1129
step_one(other_walker)
1130
if other_has_more and other_ie.file_id == file_id:
1131
# We ended up walking to this point, match and step
1133
other_values_append((other_path, other_ie))
1134
other_has_more, other_path, other_ie = \
1135
step_one(other_walker)
1137
# This record isn't in the normal order, see if it
1139
other_values_append(lookup_by_file_id(
1140
other_extra, self._other_trees[idx], file_id))
1141
next_other_entries_append((other_has_more, other_path,
1143
other_entries = next_other_entries
1145
# We've matched all the walkers, yield this datapoint
1146
yield path, file_id, master_ie, other_values
1147
self._other_walkers = other_walkers
1148
self._other_entries = other_entries
1149
self._others_extra = others_extra
1151
def _finish_others(self):
1152
"""Finish walking the other iterators, so we get all entries."""
1153
for idx, info in enumerate(self._other_entries):
1154
other_extra = self._others_extra[idx]
1155
(other_has_more, other_path, other_ie) = info
1156
while other_has_more:
1157
other_file_id = other_ie.file_id
1158
if other_file_id not in self._out_of_order_processed:
1159
other_extra[other_file_id] = (other_path, other_ie)
1160
other_has_more, other_path, other_ie = \
1161
self._step_one(self._other_walkers[idx])
1162
del self._other_entries
1164
def _walk_others(self):
1165
"""Finish up by walking all the 'deferred' nodes."""
1166
# TODO: One alternative would be to grab all possible unprocessed
1167
# file_ids, and then sort by path, and then yield them. That
1168
# might ensure better ordering, in case a caller strictly
1169
# requires parents before children.
1170
for idx, other_extra in enumerate(self._others_extra):
1171
others = sorted(other_extra.itervalues(),
1172
key=lambda x: self._path_to_key(x[0]))
1173
for other_path, other_ie in others:
1174
file_id = other_ie.file_id
1175
# We don't need to check out_of_order_processed here, because
1176
# the lookup_by_file_id will be removing anything processed
1177
# from the extras cache
1178
other_extra.pop(file_id)
1179
other_values = [(None, None) for i in xrange(idx)]
1180
other_values.append((other_path, other_ie))
1181
for alt_idx, alt_extra in enumerate(self._others_extra[idx+1:]):
1182
alt_idx = alt_idx + idx + 1
1183
alt_extra = self._others_extra[alt_idx]
1184
alt_tree = self._other_trees[alt_idx]
1185
other_values.append(self._lookup_by_file_id(
1186
alt_extra, alt_tree, file_id))
1187
yield other_path, file_id, None, other_values
272
######################################################################
275
def dir_exporter(tree, dest, root):
276
"""Export this tree to a new directory.
278
`dest` should not exist, and will be created holding the
279
contents of this tree.
281
TODO: To handle subdirectories we need to create the
284
:note: If the export fails, the destination directory will be
285
left in a half-assed state.
289
mutter('export version %r' % tree)
291
for dp, ie in inv.iter_entries():
292
ie.put_on_disk(dest, dp, tree)
294
exporters['dir'] = dir_exporter
301
def get_root_name(dest):
302
"""Get just the root name for a tarball.
304
>>> get_root_name('mytar.tar')
306
>>> get_root_name('mytar.tar.bz2')
308
>>> get_root_name('tar.tar.tar.tgz')
310
>>> get_root_name('bzr-0.0.5.tar.gz')
312
>>> get_root_name('a/long/path/mytar.tgz')
314
>>> get_root_name('../parent/../dir/other.tbz2')
317
endings = ['.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2']
318
dest = os.path.basename(dest)
320
if dest.endswith(end):
321
return dest[:-len(end)]
323
def tar_exporter(tree, dest, root, compression=None):
324
"""Export this tree to a new tar file.
326
`dest` will be created holding the contents of this tree; if it
327
already exists, it will be clobbered, like with "tar -c".
329
from time import time
331
compression = str(compression or '')
333
root = get_root_name(dest)
335
ball = tarfile.open(dest, 'w:' + compression)
336
except tarfile.CompressionError, e:
337
raise BzrError(str(e))
338
mutter('export version %r' % tree)
340
for dp, ie in inv.iter_entries():
341
mutter(" export {%s} kind %s to %s" % (ie.file_id, ie.kind, dest))
342
item, fileobj = ie.get_tar_item(root, dp, now, tree)
343
ball.addfile(item, fileobj)
346
exporters['tar'] = tar_exporter
348
def tgz_exporter(tree, dest, root):
349
tar_exporter(tree, dest, root, compression='gz')
350
exporters['tgz'] = tgz_exporter
352
def tbz_exporter(tree, dest, root):
353
tar_exporter(tree, dest, root, compression='bz2')
354
exporters['tbz2'] = tbz_exporter