1
# Copyright (C) 2005-2010 Canonical Ltd
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
17
from __future__ import absolute_import
19
from io import StringIO
25
from .tree import TreeChange
28
class TreeDelta(object):
29
"""Describes changes from one tree to another.
31
Contains seven lists with TreeChange objects.
42
Each id is listed only once.
44
Files that are both modified and renamed or copied are listed only in
45
renamed or copied, with the text_modified flag true. The text_modified
46
applies either to the content of the file or the target of the
47
symbolic link, depending of the kind of file.
49
Files are only considered renamed if their name has changed or
50
their parent directory has changed. Renaming a directory
51
does not count as renaming all its contents.
53
The lists are normally sorted when the delta is created.
61
self.kind_changed = []
67
def __eq__(self, other):
68
if not isinstance(other, TreeDelta):
70
return self.added == other.added \
71
and self.removed == other.removed \
72
and self.renamed == other.renamed \
73
and self.copied == other.copied \
74
and self.modified == other.modified \
75
and self.unchanged == other.unchanged \
76
and self.kind_changed == other.kind_changed \
77
and self.unversioned == other.unversioned
79
def __ne__(self, other):
80
return not (self == other)
83
return "TreeDelta(added=%r, removed=%r, renamed=%r," \
84
" copied=%r, kind_changed=%r, modified=%r, unchanged=%r," \
85
" unversioned=%r)" % (
86
self.added, self.removed, self.renamed, self.copied,
87
self.kind_changed, self.modified, self.unchanged,
90
def has_changed(self):
91
return bool(self.modified
98
def get_changes_as_text(self, show_ids=False, show_unchanged=False,
101
report_delta(output, self, short_status, show_ids, show_unchanged)
102
return output.getvalue()
105
def _compare_trees(old_tree, new_tree, want_unchanged, specific_files,
106
include_root, extra_trees=None,
107
require_versioned=False, want_unversioned=False):
108
"""Worker function that implements Tree.changes_from."""
110
# mutter('start compare_trees')
112
for change in new_tree.iter_changes(
113
old_tree, want_unchanged, specific_files, extra_trees=extra_trees,
114
require_versioned=require_versioned,
115
want_unversioned=want_unversioned):
116
if change.versioned == (False, False):
117
delta.unversioned.append(change)
119
if not include_root and (None, None) == change.parent_id:
121
fully_present = tuple(
122
(change.versioned[x] and change.kind[x] is not None)
124
if fully_present[0] != fully_present[1]:
125
if fully_present[1] is True:
126
delta.added.append(change)
128
if change.kind[0] == 'symlink' and not new_tree.supports_symlinks():
130
'Ignoring "%s" as symlinks '
131
'are not supported on this filesystem.' % (change.path[0],))
133
delta.removed.append(change)
134
elif fully_present[0] is False:
135
delta.missing.append(change)
136
elif change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]:
137
# If the name changes, or the parent_id changes, we have a rename or copy
138
# (if we move a parent, that doesn't count as a rename for the
141
delta.copied.append(change)
143
delta.renamed.append(change)
144
elif change.kind[0] != change.kind[1]:
145
delta.kind_changed.append(change)
146
elif change.changed_content or change.executable[0] != change.executable[1]:
147
delta.modified.append(change)
149
delta.unchanged.append(change)
151
def change_key(change):
152
if change.path[0] is None:
153
path = change.path[1]
155
path = change.path[0]
156
return (path, change.file_id)
158
delta.removed.sort(key=change_key)
159
delta.added.sort(key=change_key)
160
delta.renamed.sort(key=change_key)
161
delta.copied.sort(key=change_key)
162
delta.missing.sort(key=change_key)
163
# TODO: jam 20060529 These lists shouldn't need to be sorted
164
# since we added them in alphabetical order.
165
delta.modified.sort(key=change_key)
166
delta.unchanged.sort(key=change_key)
167
delta.unversioned.sort(key=change_key)
172
class _ChangeReporter(object):
173
"""Report changes between two trees"""
175
def __init__(self, output=None, suppress_root_add=True,
176
output_file=None, unversioned_filter=None, view_info=None,
180
:param output: a function with the signature of trace.note, i.e.
181
accepts a format and parameters.
182
:param supress_root_add: If true, adding the root will be ignored
183
(i.e. when a tree has just been initted)
184
:param output_file: If supplied, a file-like object to write to.
185
Only one of output and output_file may be supplied.
186
:param unversioned_filter: A filter function to be called on
187
unversioned files. This should return True to ignore a path.
188
By default, no filtering takes place.
189
:param view_info: A tuple of view_name,view_files if only
190
items inside a view are to be reported on, or None for
192
:param classify: Add special symbols to indicate file kind.
194
if output_file is not None:
195
if output is not None:
196
raise BzrError('Cannot specify both output and output_file')
198
def output(fmt, *args):
199
output_file.write((fmt % args) + '\n')
201
if self.output is None:
203
self.output = trace.note
204
self.suppress_root_add = suppress_root_add
205
self.modified_map = {'kind changed': 'K',
212
self.versioned_map = {'added': '+', # versioned target
213
'unchanged': ' ', # versioned in both
214
'removed': '-', # versioned in source
215
'unversioned': '?', # versioned in neither
217
self.unversioned_filter = unversioned_filter
219
self.kind_marker = osutils.kind_marker
221
self.kind_marker = lambda kind: ''
222
if view_info is None:
223
self.view_name = None
226
self.view_name = view_info[0]
227
self.view_files = view_info[1]
228
self.output("Operating on whole tree but only reporting on "
229
"'%s' view." % (self.view_name,))
231
def report(self, paths, versioned, renamed, copied, modified, exe_change,
233
"""Report one change to a file
235
:param path: The old and new paths as generated by Tree.iter_changes.
236
:param versioned: may be 'added', 'removed', 'unchanged', or
238
:param renamed: may be True or False
239
:param copied: may be True or False
240
:param modified: may be 'created', 'deleted', 'kind changed',
241
'modified' or 'unchanged'.
242
:param exe_change: True if the execute bit has changed
243
:param kind: A pair of file kinds, as generated by Tree.iter_changes.
244
None indicates no file present.
248
if paths[1] == '' and versioned == 'added' and self.suppress_root_add:
250
if self.view_files and not osutils.is_inside_any(self.view_files,
253
if versioned == 'unversioned':
254
# skip ignored unversioned files if needed.
255
if self.unversioned_filter is not None:
256
if self.unversioned_filter(paths[1]):
258
# dont show a content change in the output.
259
modified = 'unchanged'
260
# we show both paths in the following situations:
261
# the file versioning is unchanged AND
262
# ( the path is different OR
263
# the kind is different)
264
if (versioned == 'unchanged' and
265
(renamed or copied or modified == 'kind changed')):
266
if renamed or copied:
267
# on a rename or copy, we show old and new
268
old_path, path = paths
270
# if it's not renamed or copied, we're showing both for kind
271
# changes so only show the new path
272
old_path, path = paths[1], paths[1]
273
# if the file is not missing in the source, we show its kind
274
# when we show two paths.
275
if kind[0] is not None:
276
old_path += self.kind_marker(kind[0])
278
elif versioned == 'removed':
279
# not present in target
290
rename = self.versioned_map[versioned]
291
# we show the old kind on the new path when the content is deleted.
292
if modified == 'deleted':
293
path += self.kind_marker(kind[0])
294
# otherwise we always show the current kind when there is one
295
elif kind[1] is not None:
296
path += self.kind_marker(kind[1])
301
self.output("%s%s%s %s%s", rename, self.modified_map[modified], exe,
305
def report_changes(change_iterator, reporter):
306
"""Report the changes from a change iterator.
308
This is essentially a translation from low-level to medium-level changes.
309
Further processing may be required to produce a human-readable output.
310
Unfortunately, some tree-changing operations are very complex
311
:change_iterator: an iterator or sequence of changes in the format
312
generated by Tree.iter_changes
313
:param reporter: The _ChangeReporter that will report the changes.
315
versioned_change_map = {
316
(True, True): 'unchanged',
317
(True, False): 'removed',
318
(False, True): 'added',
319
(False, False): 'unversioned',
322
def path_key(change):
323
if change.path[0] is not None:
324
path = change.path[0]
326
path = change.path[1]
327
return osutils.splitpath(path)
328
for change in sorted(change_iterator, key=path_key):
330
# files are "renamed" if they are moved or if name changes, as long
332
if None not in change.name and None not in change.parent_id and\
333
(change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]):
343
if change.kind[0] != change.kind[1]:
344
if change.kind[0] is None:
346
elif change.kind[1] is None:
349
modified = "kind changed"
351
if change.changed_content:
352
modified = "modified"
353
elif change.kind[0] is None:
356
modified = "unchanged"
357
if change.kind[1] == "file":
358
exe_change = (change.executable[0] != change.executable[1])
359
versioned_change = versioned_change_map[change.versioned]
360
reporter.report(change.path, versioned_change, renamed, copied, modified,
361
exe_change, change.kind)
364
def report_delta(to_file, delta, short_status=False, show_ids=False,
365
show_unchanged=False, indent='', predicate=None, classify=True):
366
"""Output this delta in status-like form to to_file.
368
:param to_file: A file-like object where the output is displayed.
370
:param delta: A TreeDelta containing the changes to be displayed
372
:param short_status: Single-line status if True.
374
:param show_ids: Output the file ids if True.
376
:param show_unchanged: Output the unchanged files if True.
378
:param indent: Added at the beginning of all output lines (for merged
381
:param predicate: A callable receiving a path returning True if the path
384
:param classify: Add special symbols to indicate file kind.
387
def decorate_path(path, kind, meta_modified=None):
390
if kind == 'directory':
392
elif kind == 'symlink':
398
def show_more_renamed(item):
399
dec_new_path = decorate_path(item.path[1], item.kind[1], item.meta_modified())
400
to_file.write(' => %s' % dec_new_path)
401
if item.changed_content or item.meta_modified():
402
extra_modified.append(TreeChange(
403
item.file_id, (item.path[1], item.path[1]),
404
item.changed_content,
406
(item.parent_id[1], item.parent_id[1]),
407
(item.name[1], item.name[1]),
408
(item.kind[1], item.kind[1]),
411
def show_more_kind_changed(item):
412
to_file.write(' (%s => %s)' % (item.kind[0], item.kind[1]))
414
def show_path(path, kind, meta_modified,
415
default_format, with_file_id_format):
416
dec_path = decorate_path(path, kind, meta_modified)
418
to_file.write(with_file_id_format % dec_path)
420
to_file.write(default_format % dec_path)
422
def show_list(files, long_status_name, short_status_letter,
423
default_format='%s', with_file_id_format='%-30s',
428
prefix = short_status_letter
431
prefix = indent + prefix + ' '
434
if item.path[0] is None:
440
if predicate is not None and not predicate(path):
442
if not header_shown and not short_status:
443
to_file.write(indent + long_status_name + ':\n')
445
to_file.write(prefix)
446
show_path(path, kind, item.meta_modified(),
447
default_format, with_file_id_format)
448
if show_more is not None:
450
if show_ids and getattr(item, 'file_id', None):
451
to_file.write(' %s' % item.file_id.decode('utf-8'))
454
show_list(delta.removed, 'removed', 'D')
455
show_list(delta.added, 'added', 'A')
456
show_list(delta.missing, 'missing', '!')
458
show_list(delta.renamed, 'renamed', 'R', with_file_id_format='%s',
459
show_more=show_more_renamed)
460
show_list(delta.copied, 'copied', 'C', with_file_id_format='%s',
461
show_more=show_more_renamed)
462
show_list(delta.kind_changed, 'kind changed', 'K',
463
with_file_id_format='%s',
464
show_more=show_more_kind_changed)
465
show_list(delta.modified + extra_modified, 'modified', 'M')
467
show_list(delta.unchanged, 'unchanged', 'S')
469
show_list(delta.unversioned, 'unknown', ' ')