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):
92
intertree = InterTree.get(from_tree, self)
93
return intertree._iter_changes(from_tree, self, include_unchanged,
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'))
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())
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,
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
if specific_file_ids is not None:
470
specific_file_ids = set(specific_file_ids)
471
from_entries_by_dir = list(from_tree.iter_entries_by_dir())
472
from_data = dict((e.file_id, (p, e)) for p, e in from_entries_by_dir)
473
for to_path, to_entry in to_tree.iter_entries_by_dir():
474
file_id = to_entry.file_id
475
to_paths[file_id] = to_path
476
if (specific_file_ids is not None and
477
file_id not in specific_file_ids):
479
changed_content = False
480
from_path, from_entry = from_data.get(file_id, (None, None))
481
from_versioned = (from_entry is not None)
482
if from_entry is not None:
483
from_versioned = True
484
from_name = from_entry.name
485
from_parent = from_entry.parent_id
486
from_kind, from_executable, from_stat = \
487
from_tree._comparison_data(from_entry, from_path)
489
from_versioned = False
493
from_executable = None
494
versioned = (from_versioned, True)
495
to_kind, to_executable, to_stat = \
496
to_tree._comparison_data(to_entry, to_path)
497
kind = (from_kind, to_kind)
498
if kind[0] != kind[1]:
499
changed_content = True
500
elif from_kind == 'file':
501
from_size = from_tree._file_size(from_entry, from_stat)
502
to_size = to_tree._file_size(to_entry, to_stat)
503
if from_size != to_size:
504
changed_content = True
505
elif (from_tree.get_file_sha1(file_id, from_path, from_stat) !=
506
to_tree.get_file_sha1(file_id, to_path, to_stat)):
507
changed_content = True
508
elif from_kind == 'symlink':
509
if (from_tree.get_symlink_target(file_id) !=
510
to_tree.get_symlink_target(file_id)):
511
changed_content = True
512
parent = (from_parent, to_entry.parent_id)
513
name = (from_name, to_entry.name)
514
executable = (from_executable, to_executable)
515
if (changed_content is not False or versioned[0] != versioned[1]
516
or parent[0] != parent[1] or name[0] != name[1] or
517
executable[0] != executable[1] or include_unchanged):
518
yield (file_id, to_path, changed_content, versioned, parent,
519
name, kind, executable)
521
for path, from_entry in from_entries_by_dir:
522
file_id = from_entry.file_id
523
if file_id in to_paths:
525
if from_entry.parent_id is None:
528
to_path = osutils.pathjoin(to_paths[from_entry.parent_id],
530
to_paths[file_id] = to_path
531
if (specific_file_ids is not None and
532
file_id not in specific_file_ids):
534
versioned = (True, False)
535
parent = (from_entry.parent_id, None)
536
name = (from_entry.name, None)
537
from_kind, from_executable, stat_value = \
538
from_tree._comparison_data(from_entry, path)
539
kind = (from_kind, None)
540
executable = (from_executable, None)
541
changed_content = True
542
# the parent's path is necessarily known at this point.
543
yield(file_id, to_path, changed_content, versioned, parent,
544
name, kind, executable)
547
# This was deprecated before 0.12, but did not have an official warning
548
@symbol_versioning.deprecated_function(symbol_versioning.zero_twelve)
549
def RevisionTree(*args, **kwargs):
550
"""RevisionTree has moved to bzrlib.revisiontree.RevisionTree()
552
Accessing it as bzrlib.tree.RevisionTree has been deprecated as of
555
from bzrlib.revisiontree import RevisionTree as _RevisionTree
556
return _RevisionTree(*args, **kwargs)