14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from io import StringIO
23
from .tree import TreeChange
21
from bzrlib.inventory import InventoryEntry
22
from bzrlib.trace import mutter, is_quiet
23
from bzrlib.symbol_versioning import deprecated_function
26
26
class TreeDelta(object):
27
27
"""Describes changes from one tree to another.
29
Contains seven lists with TreeChange objects.
36
(oldpath, newpath, id, kind, text_modified, meta_modified)
38
(path, id, old_kind, new_kind)
40
(path, id, kind, text_modified, meta_modified)
40
46
Each id is listed only once.
42
Files that are both modified and renamed or copied are listed only in
43
renamed or copied, with the text_modified flag true. The text_modified
44
applies either to the content of the file or the target of the
48
Files that are both modified and renamed are listed only in
49
renamed, with the text_modified flag true. The text_modified
50
applies either to the the content of the file or the target of the
45
51
symbolic link, depending of the kind of file.
47
53
Files are only considered renamed if their name has changed or
51
57
The lists are normally sorted when the delta is created.
54
59
def __init__(self):
59
63
self.kind_changed = []
61
65
self.unchanged = []
62
66
self.unversioned = []
65
68
def __eq__(self, other):
66
69
if not isinstance(other, TreeDelta):
68
71
return self.added == other.added \
69
and self.removed == other.removed \
70
and self.renamed == other.renamed \
71
and self.copied == other.copied \
72
and self.modified == other.modified \
73
and self.unchanged == other.unchanged \
74
and self.kind_changed == other.kind_changed \
75
and self.unversioned == other.unversioned
72
and self.removed == other.removed \
73
and self.renamed == other.renamed \
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
77
79
def __ne__(self, other):
78
80
return not (self == other)
80
82
def __repr__(self):
81
83
return "TreeDelta(added=%r, removed=%r, renamed=%r," \
82
" copied=%r, kind_changed=%r, modified=%r, unchanged=%r," \
83
" unversioned=%r)" % (
84
self.added, self.removed, self.renamed, self.copied,
85
self.kind_changed, self.modified, self.unchanged,
84
" kind_changed=%r, modified=%r, unchanged=%r," \
85
" unversioned=%r)" % (self.added,
86
self.removed, self.renamed, self.kind_changed, self.modified,
87
self.unchanged, self.unversioned)
88
89
def has_changed(self):
89
90
return bool(self.modified
94
94
or self.kind_changed)
96
def touches_file_id(self, file_id):
97
"""Return True if file_id is modified by this delta."""
98
for l in self.added, self.removed, self.modified:
102
for v in self.renamed:
105
for v in self.kind_changed:
111
def show(self, to_file, show_ids=False, show_unchanged=False,
112
short_status=False, indent='',
114
"""Output this delta in status-like form to to_file.
116
:param to_file: A file-like object where the output is displayed.
118
:param show_ids: Output the file ids if True.
120
:param show_unchanged: Output the unchanged files if True.
122
:param short_status: Single-line status if True.
124
:param indent: Added at the beginning of all output lines (for merged
127
:param filter: A callable receiving a path and a file id and
128
returning True if the path should be displayed.
131
def decorate_path(path, kind, meta_modified=None):
132
if kind == 'directory':
134
elif kind == 'symlink':
140
def show_more_renamed(item):
141
(oldpath, file_id, kind,
142
text_modified, meta_modified, newpath) = item
143
dec_new_path = decorate_path(newpath, kind, meta_modified)
144
to_file.write(' => %s' % dec_new_path)
145
if text_modified or meta_modified:
146
extra_modified.append((newpath, file_id, kind,
147
text_modified, meta_modified))
149
def show_more_kind_changed(item):
150
(path, file_id, old_kind, new_kind) = item
151
to_file.write(' (%s => %s)' % (old_kind, new_kind))
153
def show_path(path, file_id, kind, meta_modified,
154
default_format, with_file_id_format):
155
dec_path = decorate_path(path, kind, meta_modified)
157
to_file.write(with_file_id_format % dec_path)
159
to_file.write(default_format % dec_path)
161
def show_list(files, long_status_name, short_status_letter,
162
default_format='%s', with_file_id_format='%-30s',
167
prefix = short_status_letter
170
prefix = indent + prefix + ' '
173
path, file_id, kind = item[:3]
174
if (filter is not None and not filter(path, file_id)):
176
if not header_shown and not short_status:
177
to_file.write(indent + long_status_name + ':\n')
181
meta_modified = item[4]
183
to_file.write(prefix)
184
show_path(path, file_id, kind, meta_modified,
185
default_format, with_file_id_format)
186
if show_more is not None:
189
to_file.write(' %s' % file_id)
192
show_list(self.removed, 'removed', 'D')#
193
show_list(self.added, 'added', 'A')
195
# Reorder self.renamed tuples so that all lists share the same
196
# order for their 3 first fields and that they also begin like
197
# the self.modified tuples
198
renamed = [(p, i, k, tm, mm, np)
199
for p, np, i, k, tm, mm in self.renamed]
200
show_list(renamed, 'renamed', 'R', with_file_id_format='%s',
201
show_more=show_more_renamed)
202
show_list(self.kind_changed, 'kind changed', 'K',
203
with_file_id_format='%s',
204
show_more=show_more_kind_changed)
205
show_list(self.modified + extra_modified, 'modified', 'M')
207
show_list(self.unchanged, 'unchanged', 'S')
209
show_list(self.unversioned, 'unknown', ' ')
96
211
def get_changes_as_text(self, show_ids=False, show_unchanged=False,
99
report_delta(output, self, short_status, show_ids, show_unchanged)
214
output = StringIO.StringIO()
215
self.show(output, show_ids, show_unchanged, short_status)
100
216
return output.getvalue()
107
223
delta = TreeDelta()
108
224
# mutter('start compare_trees')
110
for change in new_tree.iter_changes(
111
old_tree, want_unchanged, specific_files, extra_trees=extra_trees,
226
for (file_id, path, content_change, versioned, parent_id, name, kind,
227
executable) in new_tree.iter_changes(old_tree, want_unchanged,
228
specific_files, extra_trees=extra_trees,
112
229
require_versioned=require_versioned,
113
230
want_unversioned=want_unversioned):
114
if change.versioned == (False, False):
115
delta.unversioned.append(change)
117
if not include_root and (None, None) == change.parent_id:
119
fully_present = tuple(
120
(change.versioned[x] and change.kind[x] is not None)
231
if versioned == (False, False):
232
delta.unversioned.append((path[1], None, kind[1]))
234
if not include_root and (None, None) == parent_id:
236
fully_present = tuple((versioned[x] and kind[x] is not None) for
122
238
if fully_present[0] != fully_present[1]:
123
239
if fully_present[1] is True:
124
delta.added.append(change)
240
delta.added.append((path[1], file_id, kind[1]))
126
if change.kind[0] == 'symlink' and not new_tree.supports_symlinks():
128
'Ignoring "%s" as symlinks '
129
'are not supported on this filesystem.' % (change.path[0],))
131
delta.removed.append(change)
242
delta.removed.append((path[0], file_id, kind[0]))
132
243
elif fully_present[0] is False:
133
delta.missing.append(change)
134
elif change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]:
135
# If the name changes, or the parent_id changes, we have a rename or copy
245
elif name[0] != name[1] or parent_id[0] != parent_id[1]:
246
# If the name changes, or the parent_id changes, we have a rename
136
247
# (if we move a parent, that doesn't count as a rename for the
139
delta.copied.append(change)
141
delta.renamed.append(change)
142
elif change.kind[0] != change.kind[1]:
143
delta.kind_changed.append(change)
144
elif change.changed_content or change.executable[0] != change.executable[1]:
145
delta.modified.append(change)
147
delta.unchanged.append(change)
149
def change_key(change):
150
if change.path[0] is None:
151
path = change.path[1]
153
path = change.path[0]
154
return (path, change.file_id)
156
delta.removed.sort(key=change_key)
157
delta.added.sort(key=change_key)
158
delta.renamed.sort(key=change_key)
159
delta.copied.sort(key=change_key)
160
delta.missing.sort(key=change_key)
249
delta.renamed.append((path[0],
254
(executable[0] != executable[1])))
255
elif kind[0] != kind[1]:
256
delta.kind_changed.append((path[1], file_id, kind[0], kind[1]))
257
elif content_change or executable[0] != executable[1]:
258
delta.modified.append((path[1], file_id, kind[1],
260
(executable[0] != executable[1])))
262
delta.unchanged.append((path[1], file_id, kind[1]))
161
267
# TODO: jam 20060529 These lists shouldn't need to be sorted
162
268
# since we added them in alphabetical order.
163
delta.modified.sort(key=change_key)
164
delta.unchanged.sort(key=change_key)
165
delta.unversioned.sort(key=change_key)
269
delta.modified.sort()
270
delta.unchanged.sort()
187
291
:param view_info: A tuple of view_name,view_files if only
188
292
items inside a view are to be reported on, or None for
189
293
no view filtering.
190
:param classify: Add special symbols to indicate file kind.
192
295
if output_file is not None:
193
296
if output is not None:
194
297
raise BzrError('Cannot specify both output and output_file')
196
298
def output(fmt, *args):
197
299
output_file.write((fmt % args) + '\n')
198
300
self.output = output
199
301
if self.output is None:
302
from bzrlib import trace
201
303
self.output = trace.note
202
304
self.suppress_root_add = suppress_root_add
203
305
self.modified_map = {'kind changed': 'K',
204
306
'unchanged': ' ',
210
self.versioned_map = {'added': '+', # versioned target
211
'unchanged': ' ', # versioned in both
212
'removed': '-', # versioned in source
213
'unversioned': '?', # versioned in neither
310
self.versioned_map = {'added': '+', # versioned target
311
'unchanged': ' ', # versioned in both
312
'removed': '-', # versioned in source
313
'unversioned': '?', # versioned in neither
215
315
self.unversioned_filter = unversioned_filter
217
self.kind_marker = osutils.kind_marker
219
self.kind_marker = lambda kind: ''
220
316
if view_info is None:
221
317
self.view_name = None
222
318
self.view_files = []
226
322
self.output("Operating on whole tree but only reporting on "
227
323
"'%s' view." % (self.view_name,))
229
def report(self, paths, versioned, renamed, copied, modified, exe_change,
325
def report(self, file_id, paths, versioned, renamed, modified, exe_change,
231
327
"""Report one change to a file
329
:param file_id: The file_id of the file
233
330
:param path: The old and new paths as generated by Tree.iter_changes.
234
331
:param versioned: may be 'added', 'removed', 'unchanged', or
236
333
:param renamed: may be True or False
237
:param copied: may be True or False
238
334
:param modified: may be 'created', 'deleted', 'kind changed',
239
335
'modified' or 'unchanged'.
240
336
:param exe_change: True if the execute bit has changed
241
337
:param kind: A pair of file kinds, as generated by Tree.iter_changes.
242
338
None indicates no file present.
246
342
if paths[1] == '' and versioned == 'added' and self.suppress_root_add:
248
344
if self.view_files and not osutils.is_inside_any(self.view_files,
251
347
if versioned == 'unversioned':
252
348
# skip ignored unversioned files if needed.
311
405
:param reporter: The _ChangeReporter that will report the changes.
313
407
versioned_change_map = {
314
(True, True): 'unchanged',
315
(True, False): 'removed',
316
(False, True): 'added',
408
(True, True) : 'unchanged',
409
(True, False) : 'removed',
410
(False, True) : 'added',
317
411
(False, False): 'unversioned',
320
def path_key(change):
321
if change.path[0] is not None:
322
path = change.path[0]
324
path = change.path[1]
325
return osutils.splitpath(path)
326
for change in sorted(change_iterator, key=path_key):
413
for (file_id, path, content_change, versioned, parent_id, name, kind,
414
executable) in change_iterator:
327
415
exe_change = False
328
416
# files are "renamed" if they are moved or if name changes, as long
329
417
# as it had a value
330
if None not in change.name and None not in change.parent_id and\
331
(change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]):
418
if None not in name and None not in parent_id and\
419
(name[0] != name[1] or parent_id[0] != parent_id[1]):
341
if change.kind[0] != change.kind[1]:
342
if change.kind[0] is None:
423
if kind[0] != kind[1]:
343
425
modified = "created"
344
elif change.kind[1] is None:
426
elif kind[1] is None:
345
427
modified = "deleted"
347
429
modified = "kind changed"
349
if change.changed_content:
350
432
modified = "modified"
351
elif change.kind[0] is None:
354
434
modified = "unchanged"
355
if change.kind[1] == "file":
356
exe_change = (change.executable[0] != change.executable[1])
357
versioned_change = versioned_change_map[change.versioned]
358
reporter.report(change.path, versioned_change, renamed, copied, modified,
359
exe_change, change.kind)
362
def report_delta(to_file, delta, short_status=False, show_ids=False,
363
show_unchanged=False, indent='', predicate=None, classify=True):
364
"""Output this delta in status-like form to to_file.
366
:param to_file: A file-like object where the output is displayed.
368
:param delta: A TreeDelta containing the changes to be displayed
370
:param short_status: Single-line status if True.
372
:param show_ids: Output the file ids if True.
374
:param show_unchanged: Output the unchanged files if True.
376
:param indent: Added at the beginning of all output lines (for merged
379
:param predicate: A callable receiving a path returning True if the path
382
:param classify: Add special symbols to indicate file kind.
385
def decorate_path(path, kind, meta_modified=None):
388
if kind == 'directory':
390
elif kind == 'symlink':
396
def show_more_renamed(item):
397
dec_new_path = decorate_path(item.path[1], item.kind[1], item.meta_modified())
398
to_file.write(' => %s' % dec_new_path)
399
if item.changed_content or item.meta_modified():
400
extra_modified.append(TreeChange(
401
item.file_id, (item.path[1], item.path[1]),
402
item.changed_content,
404
(item.parent_id[1], item.parent_id[1]),
405
(item.name[1], item.name[1]),
406
(item.kind[1], item.kind[1]),
409
def show_more_kind_changed(item):
410
to_file.write(' (%s => %s)' % (item.kind[0], item.kind[1]))
412
def show_path(path, kind, meta_modified,
413
default_format, with_file_id_format):
414
dec_path = decorate_path(path, kind, meta_modified)
416
to_file.write(with_file_id_format % dec_path)
418
to_file.write(default_format % dec_path)
420
def show_list(files, long_status_name, short_status_letter,
421
default_format='%s', with_file_id_format='%-30s',
426
prefix = short_status_letter
429
prefix = indent + prefix + ' '
432
if item.path[0] is None:
438
if predicate is not None and not predicate(path):
440
if not header_shown and not short_status:
441
to_file.write(indent + long_status_name + ':\n')
443
to_file.write(prefix)
444
show_path(path, kind, item.meta_modified(),
445
default_format, with_file_id_format)
446
if show_more is not None:
448
if show_ids and getattr(item, 'file_id', None):
449
to_file.write(' %s' % item.file_id.decode('utf-8'))
452
show_list(delta.removed, 'removed', 'D')
453
show_list(delta.added, 'added', 'A')
454
show_list(delta.missing, 'missing', '!')
456
show_list(delta.renamed, 'renamed', 'R', with_file_id_format='%s',
457
show_more=show_more_renamed)
458
show_list(delta.copied, 'copied', 'C', with_file_id_format='%s',
459
show_more=show_more_renamed)
460
show_list(delta.kind_changed, 'kind changed', 'K',
461
with_file_id_format='%s',
462
show_more=show_more_kind_changed)
463
show_list(delta.modified + extra_modified, 'modified', 'M')
465
show_list(delta.unchanged, 'unchanged', 'S')
467
show_list(delta.unversioned, 'unknown', ' ')
435
if kind[1] == "file":
436
exe_change = (executable[0] != executable[1])
437
versioned_change = versioned_change_map[versioned]
438
reporter.report(file_id, path, versioned_change, renamed, modified,