50
53
trees or versioned trees.
56
def changes_from(self, other, want_unchanged=False, specific_files=None,
57
extra_trees=None, require_versioned=False):
58
"""Return a TreeDelta of the changes from other to this tree.
60
:param other: A tree to compare with.
61
:param specific_files: An optional list of file paths to restrict the
62
comparison to. When mapping filenames to ids, all matches in all
63
trees (including optional extra_trees) are used, and all children of
64
matched directories are included.
65
:param want_unchanged: An optional boolean requesting the inclusion of
66
unchanged entries in the result.
67
:param extra_trees: An optional list of additional trees to use when
68
mapping the contents of specific_files (paths) to file_ids.
69
:param require_versioned: An optional boolean (defaults to False). When
70
supplied and True all the 'specific_files' must be versioned, or
71
a PathsNotVersionedError will be thrown.
73
The comparison will be performed by an InterTree object looked up on
76
# Martin observes that Tree.changes_from returns a TreeDelta and this
77
# may confuse people, because the class name of the returned object is
78
# a synonym of the object referenced in the method name.
79
return InterTree.get(other, self).compare(
80
want_unchanged=want_unchanged,
81
specific_files=specific_files,
82
extra_trees=extra_trees,
83
require_versioned=require_versioned,
86
def iter_changes(self, from_tree, include_unchanged=False,
87
specific_file_ids=None):
88
intertree = InterTree.get(from_tree, self)
89
return intertree.iter_changes(from_tree, self, include_unchanged,
93
"""Get a list of the conflicts in the tree.
95
Each conflict is an instance of bzrlib.conflicts.Conflict.
99
def get_parent_ids(self):
100
"""Get the parent ids for this tree.
102
:return: a list of parent ids. [] is returned to indicate
103
a tree with no parents.
104
:raises: BzrError if the parents are not known.
106
raise NotImplementedError(self.get_parent_ids)
53
108
def has_filename(self, filename):
54
109
"""True if the tree has given filename."""
55
110
raise NotImplementedError()
92
167
"store is probably damaged/corrupt"])
95
def print_file(self, fileid):
96
"""Print file with id `fileid` to stdout."""
170
def print_file(self, file_id):
171
"""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
173
sys.stdout.write(self.get_file_text(file_id))
179
"""What files are present in this tree and unknown.
181
:return: an iterator over the unknown files.
188
def filter_unversioned_files(self, paths):
189
"""Filter out paths that are not versioned.
191
:return: set of paths.
193
# NB: we specifically *don't* call self.has_filename, because for
194
# WorkingTrees that can indicate files that exist on disk but that
196
pred = self.inventory.has_filename
197
return set((p for p in paths if not pred(p)))
201
from bzrlib.revisiontree import RevisionTree
149
204
class EmptyTree(Tree):
150
206
def __init__(self):
151
207
self._inventory = Inventory()
208
warn('EmptyTree is deprecated as of bzr 0.9 please use '
209
'repository.revision_tree instead.',
210
DeprecationWarning, stacklevel=2)
212
def get_parent_ids(self):
215
def get_symlink_target(self, file_id):
153
218
def has_filename(self, filename):
221
def kind(self, file_id):
222
assert self._inventory[file_id].kind == "directory"
156
225
def list_files(self):
157
if False: # just to make it a generator
228
def __contains__(self, file_id):
229
return (file_id in self._inventory)
231
def get_file_sha1(self, file_id, path=None):
162
235
######################################################################
224
297
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())
300
def find_ids_across_trees(filenames, trees, require_versioned=True):
301
"""Find the ids corresponding to specified filenames.
303
All matches in all trees will be used, and all children of matched
304
directories will be used.
306
:param filenames: The filenames to find file_ids for
307
:param trees: The trees to find file_ids within
308
:param require_versioned: if true, all specified filenames must occur in
310
:return: a set of file ids for the specified filenames and their children.
314
specified_ids = _find_filename_ids_across_trees(filenames, trees,
316
return _find_children_across_trees(specified_ids, trees)
319
def _find_filename_ids_across_trees(filenames, trees, require_versioned):
320
"""Find the ids corresponding to specified filenames.
322
All matches in all trees will be used.
324
:param filenames: The filenames to find file_ids for
325
:param trees: The trees to find file_ids within
326
:param require_versioned: if true, all specified filenames must occur in
328
:return: a set of file ids for the specified filenames
331
interesting_ids = set()
332
for tree_path in filenames:
335
file_id = tree.inventory.path2id(tree_path)
336
if file_id is not None:
337
interesting_ids.add(file_id)
340
not_versioned.append(tree_path)
341
if len(not_versioned) > 0 and require_versioned:
342
raise errors.PathsNotVersionedError(not_versioned)
343
return interesting_ids
346
def _find_children_across_trees(specified_ids, trees):
347
"""Return a set including specified ids and their children
349
All matches in all trees will be used.
351
:param trees: The trees to find file_ids within
352
:return: a set containing all specified ids and their children
354
interesting_ids = set(specified_ids)
355
pending = interesting_ids
356
# now handle children of interesting ids
357
# we loop so that we handle all children of each id in both trees
358
while len(pending) > 0:
360
for file_id in pending:
362
if file_id not in tree:
364
entry = tree.inventory[file_id]
365
for child in getattr(entry, 'children', {}).itervalues():
366
if child.file_id not in interesting_ids:
367
new_pending.add(child.file_id)
368
interesting_ids.update(new_pending)
369
pending = new_pending
370
return interesting_ids
373
class InterTree(InterObject):
374
"""This class represents operations taking place between two Trees.
376
Its instances have methods like 'compare' and contain references to the
377
source and target trees these operations are to be carried out on.
379
clients of bzrlib should not need to use InterTree directly, rather they
380
should use the convenience methods on Tree such as 'Tree.compare()' which
381
will pass through to InterTree as appropriate.
387
def compare(self, want_unchanged=False, specific_files=None,
388
extra_trees=None, require_versioned=False):
389
"""Return the changes from source to target.
391
:return: A TreeDelta.
392
:param specific_files: An optional list of file paths to restrict the
393
comparison to. When mapping filenames to ids, all matches in all
394
trees (including optional extra_trees) are used, and all children of
395
matched directories are included.
396
:param want_unchanged: An optional boolean requesting the inclusion of
397
unchanged entries in the result.
398
:param extra_trees: An optional list of additional trees to use when
399
mapping the contents of specific_files (paths) to file_ids.
400
:param require_versioned: An optional boolean (defaults to False). When
401
supplied and True all the 'specific_files' must be versioned, or
402
a PathsNotVersionedError will be thrown.
404
# NB: show_status depends on being able to pass in non-versioned files and
405
# report them as unknown
406
trees = (self.source, self.target)
407
if extra_trees is not None:
408
trees = trees + tuple(extra_trees)
409
specific_file_ids = find_ids_across_trees(specific_files,
410
trees, require_versioned=require_versioned)
411
if specific_files and not specific_file_ids:
412
# All files are unversioned, so just return an empty delta
413
# _compare_trees would think we want a complete delta
414
return delta.TreeDelta()
415
return delta._compare_trees(self.source, self.target, want_unchanged,
418
def iter_changes(self, from_tree, to_tree, include_unchanged,
420
"""Generate an iterator of changes between trees.
423
(file_id, path, changed_content, versioned, parent, name, kind,
426
Path is relative to the to_tree. changed_content is True if the file's
427
content has changed. This includes changes to its kind, and to
430
versioned, parent, name, kind, executable are tuples of (from, to) if
431
changed. If a file is missing in a tree, its kind is None.
433
Iteration is done in parent-to-child order, relative to the to_tree.
435
def get_versioned_kind(tree, file_id):
437
return tree.kind(file_id)
438
except errors.NoSuchFile:
442
if specific_file_ids is not None:
443
specific_file_ids = set(specific_file_ids)
444
for path, to_entry in to_tree.iter_entries_by_dir():
445
file_id = to_entry.file_id
446
to_paths[file_id] = path
447
if (specific_file_ids is not None and
448
file_id not in specific_file_ids):
450
changed_content = False
451
from_versioned = (file_id in from_tree)
452
versioned = (from_versioned, True)
454
from_kind = get_versioned_kind(from_tree, file_id)
455
if from_kind is not None:
456
from_executable = (from_tree.is_executable(file_id) not in
459
from_executable = None
460
from_entry = from_tree.inventory[file_id]
461
from_parent = from_entry.parent_id
462
from_name = from_entry.name
467
from_executable = None
468
to_kind = get_versioned_kind(to_tree, file_id)
469
kind = (from_kind, to_kind)
470
if kind[0] != kind[1]:
471
changed_content = True
472
elif from_kind == 'file':
473
if (from_tree.get_file_sha1(file_id) !=
474
to_tree.get_file_sha1(file_id)):
475
changed_content = True
476
elif from_kind == 'symlink':
477
if (from_tree.get_symlink_target(file_id) !=
478
to_tree.get_symlink_target(file_id)):
479
changed_content = True
480
parent = (from_parent, to_entry.parent_id)
481
name = (from_name, to_entry.name)
482
if to_kind is not None:
483
to_executable = (to_tree.is_executable(file_id) not in
487
executable = (from_executable, to_executable)
488
if (changed_content is not False or versioned[0] != versioned[1]
489
or parent[0] != parent[1] or name[0] != name[1] or
490
executable[0] != executable[1] or include_unchanged):
491
yield (file_id, path, changed_content, versioned, parent,
492
name, kind, executable)
494
for path, from_entry in from_tree.iter_entries_by_dir():
495
file_id = from_entry.file_id
496
if file_id in to_paths:
498
to_path = osutils.pathjoin(to_paths[from_entry.parent_id],
500
to_paths[file_id] = to_path
501
if (specific_file_ids is not None and
502
file_id not in specific_file_ids):
504
versioned = (True, False)
505
parent = (from_entry.parent_id, None)
506
name = (from_entry.name, None)
507
kind = (get_versioned_kind(from_tree, file_id), None)
508
if kind[0] is not None:
509
from_executable = (from_tree.is_executable(file_id) not in
512
from_executable = False
513
executable = (from_executable, None)
514
changed_content = True
515
# the parent's path is necessarily known at this point.
516
yield(file_id, to_path, changed_content, versioned, parent,
517
name, kind, executable)