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
17
from __future__ import absolute_import
23
from .tree import TreeChange
22
from brzlib.trace import is_quiet
26
25
class TreeDelta(object):
27
26
"""Describes changes from one tree to another.
29
Contains seven lists with TreeChange objects.
35
(oldpath, newpath, id, kind, text_modified, meta_modified)
37
(path, id, old_kind, new_kind)
39
(path, id, kind, text_modified, meta_modified)
40
45
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
47
Files that are both modified and renamed are listed only in
48
renamed, with the text_modified flag true. The text_modified
44
49
applies either to the content of the file or the target of the
45
50
symbolic link, depending of the kind of file.
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:
96
110
def get_changes_as_text(self, show_ids=False, show_unchanged=False,
97
111
short_status=False):
113
output = StringIO.StringIO()
99
114
report_delta(output, self, short_status, show_ids, show_unchanged)
100
115
return output.getvalue()
107
122
delta = TreeDelta()
108
123
# mutter('start compare_trees')
110
for change in new_tree.iter_changes(
111
old_tree, want_unchanged, specific_files, extra_trees=extra_trees,
125
for (file_id, path, content_change, versioned, parent_id, name, kind,
126
executable) in new_tree.iter_changes(old_tree, want_unchanged,
127
specific_files, extra_trees=extra_trees,
112
128
require_versioned=require_versioned,
113
129
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)
130
if versioned == (False, False):
131
delta.unversioned.append((path[1], None, kind[1]))
133
if not include_root and (None, None) == parent_id:
135
fully_present = tuple((versioned[x] and kind[x] is not None) for
122
137
if fully_present[0] != fully_present[1]:
123
138
if fully_present[1] is True:
124
delta.added.append(change)
139
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)
141
delta.removed.append((path[0], file_id, kind[0]))
132
142
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
143
delta.missing.append((path[1], file_id, kind[1]))
144
elif name[0] != name[1] or parent_id[0] != parent_id[1]:
145
# If the name changes, or the parent_id changes, we have a rename
136
146
# (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)
148
delta.renamed.append((path[0],
153
(executable[0] != executable[1])))
154
elif kind[0] != kind[1]:
155
delta.kind_changed.append((path[1], file_id, kind[0], kind[1]))
156
elif content_change or executable[0] != executable[1]:
157
delta.modified.append((path[1], file_id, kind[1],
159
(executable[0] != executable[1])))
161
delta.unchanged.append((path[1], file_id, kind[1]))
161
167
# TODO: jam 20060529 These lists shouldn't need to be sorted
162
168
# 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)
169
delta.modified.sort()
170
delta.unchanged.sort()
210
self.versioned_map = {'added': '+', # versioned target
211
'unchanged': ' ', # versioned in both
212
'removed': '-', # versioned in source
213
'unversioned': '?', # versioned in neither
214
self.versioned_map = {'added': '+', # versioned target
215
'unchanged': ' ', # versioned in both
216
'removed': '-', # versioned in source
217
'unversioned': '?', # versioned in neither
215
219
self.unversioned_filter = unversioned_filter
226
230
self.output("Operating on whole tree but only reporting on "
227
231
"'%s' view." % (self.view_name,))
229
def report(self, paths, versioned, renamed, copied, modified, exe_change,
233
def report(self, file_id, paths, versioned, renamed, modified, exe_change,
231
235
"""Report one change to a file
237
:param file_id: The file_id of the file
233
238
:param path: The old and new paths as generated by Tree.iter_changes.
234
239
:param versioned: may be 'added', 'removed', 'unchanged', or
236
241
:param renamed: may be True or False
237
:param copied: may be True or False
238
242
:param modified: may be 'created', 'deleted', 'kind changed',
239
243
'modified' or 'unchanged'.
240
244
:param exe_change: True if the execute bit has changed
241
245
:param kind: A pair of file kinds, as generated by Tree.iter_changes.
242
246
None indicates no file present.
246
250
if paths[1] == '' and versioned == 'added' and self.suppress_root_add:
248
252
if self.view_files and not osutils.is_inside_any(self.view_files,
251
255
if versioned == 'unversioned':
252
256
# skip ignored unversioned files if needed.
260
264
# ( the path is different OR
261
265
# the kind is different)
262
266
if (versioned == 'unchanged' and
263
(renamed or copied or modified == 'kind changed')):
264
if renamed or copied:
265
# on a rename or copy, we show old and new
267
(renamed or modified == 'kind changed')):
269
# on a rename, we show old and new
266
270
old_path, path = paths
268
# if it's not renamed or copied, we're showing both for kind
269
# changes so only show the new path
272
# if it's not renamed, we're showing both for kind changes
273
# so only show the new path
270
274
old_path, path = paths[1], paths[1]
271
275
# if the file is not missing in the source, we show its kind
272
276
# when we show two paths.
311
312
:param reporter: The _ChangeReporter that will report the changes.
313
314
versioned_change_map = {
314
(True, True): 'unchanged',
315
(True, False): 'removed',
316
(False, True): 'added',
315
(True, True) : 'unchanged',
316
(True, False) : 'removed',
317
(False, True) : 'added',
317
318
(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):
320
for (file_id, path, content_change, versioned, parent_id, name, kind,
321
executable) in change_iterator:
327
322
exe_change = False
328
323
# files are "renamed" if they are moved or if name changes, as long
329
324
# 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]):
325
if None not in name and None not in parent_id and\
326
(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:
330
if kind[0] != kind[1]:
343
332
modified = "created"
344
elif change.kind[1] is None:
333
elif kind[1] is None:
345
334
modified = "deleted"
347
336
modified = "kind changed"
349
if change.changed_content:
350
339
modified = "modified"
351
elif change.kind[0] is None:
340
elif kind[0] is None:
352
341
modified = "missing"
354
343
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):
344
if kind[1] == "file":
345
exe_change = (executable[0] != executable[1])
346
versioned_change = versioned_change_map[versioned]
347
reporter.report(file_id, path, versioned_change, renamed, modified,
350
def report_delta(to_file, delta, short_status=False, show_ids=False,
351
show_unchanged=False, indent='', filter=None, classify=True):
364
352
"""Output this delta in status-like form to to_file.
366
354
:param to_file: A file-like object where the output is displayed.
396
384
def show_more_renamed(item):
397
dec_new_path = decorate_path(item.path[1], item.kind[1], item.meta_modified())
385
(oldpath, file_id, kind,
386
text_modified, meta_modified, newpath) = item
387
dec_new_path = decorate_path(newpath, kind, meta_modified)
398
388
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]),
389
if text_modified or meta_modified:
390
extra_modified.append((newpath, file_id, kind,
391
text_modified, meta_modified))
409
393
def show_more_kind_changed(item):
410
to_file.write(' (%s => %s)' % (item.kind[0], item.kind[1]))
394
(path, file_id, old_kind, new_kind) = item
395
to_file.write(' (%s => %s)' % (old_kind, new_kind))
412
def show_path(path, kind, meta_modified,
397
def show_path(path, file_id, kind, meta_modified,
413
398
default_format, with_file_id_format):
414
399
dec_path = decorate_path(path, kind, meta_modified)
429
414
prefix = indent + prefix + ' '
431
416
for item in files:
432
if item.path[0] is None:
438
if predicate is not None and not predicate(path):
417
path, file_id, kind = item[:3]
418
if (filter is not None and not filter(path, file_id)):
440
420
if not header_shown and not short_status:
441
421
to_file.write(indent + long_status_name + ':\n')
442
422
header_shown = True
425
meta_modified = item[4]
443
427
to_file.write(prefix)
444
show_path(path, kind, item.meta_modified(),
428
show_path(path, file_id, kind, meta_modified,
445
429
default_format, with_file_id_format)
446
430
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'))
433
to_file.write(' %s' % file_id)
450
434
to_file.write('\n')
452
436
show_list(delta.removed, 'removed', 'D')
453
437
show_list(delta.added, 'added', 'A')
454
438
show_list(delta.missing, 'missing', '!')
455
439
extra_modified = []
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',
440
# Reorder delta.renamed tuples so that all lists share the same
441
# order for their 3 first fields and that they also begin like
442
# the delta.modified tuples
443
renamed = [(p, i, k, tm, mm, np)
444
for p, np, i, k, tm, mm in delta.renamed]
445
show_list(renamed, 'renamed', 'R', with_file_id_format='%s',
459
446
show_more=show_more_renamed)
460
447
show_list(delta.kind_changed, 'kind changed', 'K',
461
448
with_file_id_format='%s',