50
56
trees or versioned trees.
59
def changes_from(self, other, want_unchanged=False, specific_files=None,
60
extra_trees=None, require_versioned=False, include_root=False):
61
"""Return a TreeDelta of the changes from other to this tree.
63
:param other: A tree to compare with.
64
:param specific_files: An optional list of file paths to restrict the
65
comparison to. When mapping filenames to ids, all matches in all
66
trees (including optional extra_trees) are used, and all children of
67
matched directories are included.
68
:param want_unchanged: An optional boolean requesting the inclusion of
69
unchanged entries in the result.
70
:param extra_trees: An optional list of additional trees to use when
71
mapping the contents of specific_files (paths) to file_ids.
72
:param require_versioned: An optional boolean (defaults to False). When
73
supplied and True all the 'specific_files' must be versioned, or
74
a PathsNotVersionedError will be thrown.
76
The comparison will be performed by an InterTree object looked up on
79
# Martin observes that Tree.changes_from returns a TreeDelta and this
80
# may confuse people, because the class name of the returned object is
81
# a synonym of the object referenced in the method name.
82
return InterTree.get(other, self).compare(
83
want_unchanged=want_unchanged,
84
specific_files=specific_files,
85
extra_trees=extra_trees,
86
require_versioned=require_versioned,
87
include_root=include_root
90
def _iter_changes(self, from_tree, include_unchanged=False,
91
specific_file_ids=None, pb=None):
92
intertree = InterTree.get(from_tree, self)
93
return intertree._iter_changes(from_tree, self, include_unchanged,
94
specific_file_ids, pb)
97
"""Get a list of the conflicts in the tree.
99
Each conflict is an instance of bzrlib.conflicts.Conflict.
103
def get_parent_ids(self):
104
"""Get the parent ids for this tree.
106
:return: a list of parent ids. [] is returned to indicate
107
a tree with no parents.
108
:raises: BzrError if the parents are not known.
110
raise NotImplementedError(self.get_parent_ids)
53
112
def has_filename(self, filename):
54
113
"""True if the tree has given filename."""
55
114
raise NotImplementedError()
91
200
"file is actually %s" % fp['sha1'],
92
201
"store is probably damaged/corrupt"])
203
def path2id(self, path):
204
"""Return the id for path in this tree."""
205
return self._inventory.path2id(path)
95
def print_file(self, fileid):
96
"""Print file with id `fileid` to stdout."""
207
def print_file(self, file_id):
208
"""Print file with id `file_id` to stdout."""
98
pumpfile(self.get_file(fileid), sys.stdout)
101
def export(self, dest, format='dir', root=None):
102
"""Export this tree."""
104
exporter = exporters[format]
106
from bzrlib.errors import BzrCommandError
107
raise BzrCommandError("export format %r not supported" % format)
108
exporter(self, dest, root)
112
class RevisionTree(Tree):
113
"""Tree viewing a previous revision.
115
File text can be retrieved from the text store.
117
TODO: Some kind of `__repr__` method, but a good one
118
probably means knowing the branch and revision number,
119
or at least passing a description to the constructor.
122
def __init__(self, store, inv):
124
self._inventory = inv
126
def get_file(self, file_id):
127
ie = self._inventory[file_id]
128
f = self._store[ie.text_id]
129
mutter(" get fileid{%s} from %r" % (file_id, self))
130
self._check_retrieved(ie, f)
133
def get_file_size(self, file_id):
134
return self._inventory[file_id].text_size
136
def get_file_sha1(self, file_id):
137
ie = self._inventory[file_id]
140
def has_filename(self, filename):
141
return bool(self.inventory.path2id(filename))
143
def list_files(self):
144
# The only files returned by this are those from the version
145
for path, entry in self.inventory.iter_entries():
146
yield path, 'V', entry.kind, entry.file_id
210
sys.stdout.write(self.get_file_text(file_id))
216
"""What files are present in this tree and unknown.
218
:return: an iterator over the unknown files.
225
def filter_unversioned_files(self, paths):
226
"""Filter out paths that are not versioned.
228
:return: set of paths.
230
# NB: we specifically *don't* call self.has_filename, because for
231
# WorkingTrees that can indicate files that exist on disk but that
233
pred = self.inventory.has_filename
234
return set((p for p in paths if not pred(p)))
149
237
class EmptyTree(Tree):
150
239
def __init__(self):
151
self._inventory = Inventory()
240
self._inventory = Inventory(root_id=None)
241
symbol_versioning.warn('EmptyTree is deprecated as of bzr 0.9 please'
242
' use repository.revision_tree instead.',
243
DeprecationWarning, stacklevel=2)
245
def get_parent_ids(self):
248
def get_symlink_target(self, file_id):
153
251
def has_filename(self, filename):
156
def list_files(self):
157
if False: # just to make it a generator
254
def kind(self, file_id):
255
assert self._inventory[file_id].kind == "directory"
258
def list_files(self, include_root=False):
261
def __contains__(self, file_id):
262
return (file_id in self._inventory)
264
def get_file_sha1(self, file_id, path=None, stat_value=None):
162
268
######################################################################
224
330
yield (old_name, new_name)
228
######################################################################
231
def dir_exporter(tree, dest, root):
232
"""Export this tree to a new directory.
234
`dest` should not exist, and will be created holding the
235
contents of this tree.
237
TODO: To handle subdirectories we need to create the
240
:note: If the export fails, the destination directory will be
241
left in a half-assed state.
245
mutter('export version %r' % tree)
247
for dp, ie in inv.iter_entries():
249
fullpath = appendpath(dest, dp)
250
if kind == 'directory':
253
pumpfile(tree.get_file(ie.file_id), file(fullpath, 'wb'))
333
def find_ids_across_trees(filenames, trees, require_versioned=True):
334
"""Find the ids corresponding to specified filenames.
336
All matches in all trees will be used, and all children of matched
337
directories will be used.
339
:param filenames: The filenames to find file_ids for
340
:param trees: The trees to find file_ids within
341
:param require_versioned: if true, all specified filenames must occur in
343
:return: a set of file ids for the specified filenames and their children.
347
specified_ids = _find_filename_ids_across_trees(filenames, trees,
349
return _find_children_across_trees(specified_ids, trees)
352
def _find_filename_ids_across_trees(filenames, trees, require_versioned):
353
"""Find the ids corresponding to specified filenames.
355
All matches in all trees will be used.
357
:param filenames: The filenames to find file_ids for
358
:param trees: The trees to find file_ids within
359
:param require_versioned: if true, all specified filenames must occur in
361
:return: a set of file ids for the specified filenames
364
interesting_ids = set()
365
for tree_path in filenames:
368
file_id = tree.inventory.path2id(tree_path)
369
if file_id is not None:
370
interesting_ids.add(file_id)
373
not_versioned.append(tree_path)
374
if len(not_versioned) > 0 and require_versioned:
375
raise errors.PathsNotVersionedError(not_versioned)
376
return interesting_ids
379
def _find_children_across_trees(specified_ids, trees):
380
"""Return a set including specified ids and their children
382
All matches in all trees will be used.
384
:param trees: The trees to find file_ids within
385
:return: a set containing all specified ids and their children
387
interesting_ids = set(specified_ids)
388
pending = interesting_ids
389
# now handle children of interesting ids
390
# we loop so that we handle all children of each id in both trees
391
while len(pending) > 0:
393
for file_id in pending:
395
if file_id not in tree:
397
entry = tree.inventory[file_id]
398
for child in getattr(entry, 'children', {}).itervalues():
399
if child.file_id not in interesting_ids:
400
new_pending.add(child.file_id)
401
interesting_ids.update(new_pending)
402
pending = new_pending
403
return interesting_ids
406
class InterTree(InterObject):
407
"""This class represents operations taking place between two Trees.
409
Its instances have methods like 'compare' and contain references to the
410
source and target trees these operations are to be carried out on.
412
clients of bzrlib should not need to use InterTree directly, rather they
413
should use the convenience methods on Tree such as 'Tree.compare()' which
414
will pass through to InterTree as appropriate.
420
def compare(self, want_unchanged=False, specific_files=None,
421
extra_trees=None, require_versioned=False, include_root=False):
422
"""Return the changes from source to target.
424
:return: A TreeDelta.
425
:param specific_files: An optional list of file paths to restrict the
426
comparison to. When mapping filenames to ids, all matches in all
427
trees (including optional extra_trees) are used, and all children of
428
matched directories are included.
429
:param want_unchanged: An optional boolean requesting the inclusion of
430
unchanged entries in the result.
431
:param extra_trees: An optional list of additional trees to use when
432
mapping the contents of specific_files (paths) to file_ids.
433
:param require_versioned: An optional boolean (defaults to False). When
434
supplied and True all the 'specific_files' must be versioned, or
435
a PathsNotVersionedError will be thrown.
437
# NB: show_status depends on being able to pass in non-versioned files and
438
# report them as unknown
439
trees = (self.source, self.target)
440
if extra_trees is not None:
441
trees = trees + tuple(extra_trees)
442
specific_file_ids = find_ids_across_trees(specific_files,
443
trees, require_versioned=require_versioned)
444
if specific_files and not specific_file_ids:
445
# All files are unversioned, so just return an empty delta
446
# _compare_trees would think we want a complete delta
447
return delta.TreeDelta()
448
return delta._compare_trees(self.source, self.target, want_unchanged,
449
specific_file_ids, include_root)
451
def _iter_changes(self, from_tree, to_tree, include_unchanged,
452
specific_file_ids, pb):
453
"""Generate an iterator of changes between trees.
456
(file_id, path, changed_content, versioned, parent, name, kind,
459
Path is relative to the to_tree. changed_content is True if the file's
460
content has changed. This includes changes to its kind, and to
463
versioned, parent, name, kind, executable are tuples of (from, to).
464
If a file is missing in a tree, its kind is None.
466
Iteration is done in parent-to-child order, relative to the to_tree.
469
from_entries_by_dir = list(from_tree.inventory.iter_entries_by_dir())
470
from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
471
to_entries_by_dir = list(to_tree.inventory.iter_entries_by_dir())
472
if specific_file_ids is not None:
473
specific_file_ids = set(specific_file_ids)
474
num_entries = len(specific_file_ids)
255
raise BzrError("don't know how to export {%s} of kind %r" % (ie.file_id, kind))
256
mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath))
257
exporters['dir'] = dir_exporter
264
def get_root_name(dest):
265
"""Get just the root name for a tarball.
267
>>> get_root_name('mytar.tar')
269
>>> get_root_name('mytar.tar.bz2')
271
>>> get_root_name('tar.tar.tar.tgz')
273
>>> get_root_name('bzr-0.0.5.tar.gz')
275
>>> get_root_name('a/long/path/mytar.tgz')
277
>>> get_root_name('../parent/../dir/other.tbz2')
280
endings = ['.tar', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2']
281
dest = os.path.basename(dest)
283
if dest.endswith(end):
284
return dest[:-len(end)]
286
def tar_exporter(tree, dest, root, compression=None):
287
"""Export this tree to a new tar file.
289
`dest` will be created holding the contents of this tree; if it
290
already exists, it will be clobbered, like with "tar -c".
292
from time import time
294
compression = str(compression or '')
296
root = get_root_name(dest)
298
ball = tarfile.open(dest, 'w:' + compression)
299
except tarfile.CompressionError, e:
300
raise BzrError(str(e))
301
mutter('export version %r' % tree)
303
for dp, ie in inv.iter_entries():
304
mutter(" export {%s} kind %s to %s" % (ie.file_id, ie.kind, dest))
305
item = tarfile.TarInfo(os.path.join(root, dp))
306
# TODO: would be cool to actually set it to the timestamp of the
307
# revision it was last changed
309
if ie.kind == 'directory':
310
item.type = tarfile.DIRTYPE
315
elif ie.kind == 'file':
316
item.type = tarfile.REGTYPE
317
fileobj = tree.get_file(ie.file_id)
318
item.size = _find_file_size(fileobj)
321
raise BzrError("don't know how to export {%s} of kind %r" %
322
(ie.file_id, ie.kind))
324
ball.addfile(item, fileobj)
326
exporters['tar'] = tar_exporter
328
def tgz_exporter(tree, dest, root):
329
tar_exporter(tree, dest, root, compression='gz')
330
exporters['tgz'] = tgz_exporter
332
def tbz_exporter(tree, dest, root):
333
tar_exporter(tree, dest, root, compression='bz2')
334
exporters['tbz2'] = tbz_exporter
337
def _find_file_size(fileobj):
338
offset = fileobj.tell()
341
size = fileobj.tell()
343
# gzip doesn't accept second argument to seek()
347
nread = len(fileobj.read())
476
num_entries = len(from_entries_by_dir) + len(to_entries_by_dir)
478
for to_path, to_entry in to_entries_by_dir:
479
file_id = to_entry.file_id
480
to_paths[file_id] = to_path
481
if (specific_file_ids is not None and
482
file_id not in specific_file_ids):
485
changed_content = False
486
from_path, from_entry = from_data.get(file_id, (None, None))
487
from_versioned = (from_entry is not None)
488
if from_entry is not None:
489
from_versioned = True
490
from_name = from_entry.name
491
from_parent = from_entry.parent_id
492
from_kind, from_executable, from_stat = \
493
from_tree._comparison_data(from_entry, from_path)
494
if specific_file_ids is None:
497
from_versioned = False
501
from_executable = None
502
versioned = (from_versioned, True)
503
to_kind, to_executable, to_stat = \
504
to_tree._comparison_data(to_entry, to_path)
505
kind = (from_kind, to_kind)
506
if kind[0] != kind[1]:
507
changed_content = True
508
elif from_kind == 'file':
509
from_size = from_tree._file_size(from_entry, from_stat)
510
to_size = to_tree._file_size(to_entry, to_stat)
511
if from_size != to_size:
512
changed_content = True
513
elif (from_tree.get_file_sha1(file_id, from_path, from_stat) !=
514
to_tree.get_file_sha1(file_id, to_path, to_stat)):
515
changed_content = True
516
elif from_kind == 'symlink':
517
if (from_tree.get_symlink_target(file_id) !=
518
to_tree.get_symlink_target(file_id)):
519
changed_content = True
520
parent = (from_parent, to_entry.parent_id)
521
name = (from_name, to_entry.name)
522
executable = (from_executable, to_executable)
524
pb.update('comparing files', entry_count, num_entries)
525
if (changed_content is not False or versioned[0] != versioned[1]
526
or parent[0] != parent[1] or name[0] != name[1] or
527
executable[0] != executable[1] or include_unchanged):
528
yield (file_id, to_path, changed_content, versioned, parent,
529
name, kind, executable)
531
for path, from_entry in from_entries_by_dir:
532
file_id = from_entry.file_id
533
if file_id in to_paths:
535
if from_entry.parent_id is None:
538
to_path = osutils.pathjoin(to_paths[from_entry.parent_id],
540
to_paths[file_id] = to_path
541
if (specific_file_ids is not None and
542
file_id not in specific_file_ids):
546
pb.update('comparing files', entry_count, num_entries)
547
versioned = (True, False)
548
parent = (from_entry.parent_id, None)
549
name = (from_entry.name, None)
550
from_kind, from_executable, stat_value = \
551
from_tree._comparison_data(from_entry, path)
552
kind = (from_kind, None)
553
executable = (from_executable, None)
554
changed_content = True
555
# the parent's path is necessarily known at this point.
556
yield(file_id, to_path, changed_content, versioned, parent,
557
name, kind, executable)
560
# This was deprecated before 0.12, but did not have an official warning
561
@symbol_versioning.deprecated_function(symbol_versioning.zero_twelve)
562
def RevisionTree(*args, **kwargs):
563
"""RevisionTree has moved to bzrlib.revisiontree.RevisionTree()
565
Accessing it as bzrlib.tree.RevisionTree has been deprecated as of
568
from bzrlib.revisiontree import RevisionTree as _RevisionTree
569
return _RevisionTree(*args, **kwargs)