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 __future__ import absolute_import
20
from bzrlib.trace import is_quiet
26
from .tree import TreeChange
23
29
class TreeDelta(object):
24
30
"""Describes changes from one tree to another.
32
Contains seven lists with TreeChange objects.
33
(oldpath, newpath, id, kind, text_modified, meta_modified)
35
(path, id, old_kind, new_kind)
37
(path, id, kind, text_modified, meta_modified)
43
42
Each id is listed only once.
62
62
self.unchanged = []
63
63
self.unversioned = []
65
66
def __eq__(self, other):
66
67
if not isinstance(other, TreeDelta):
68
69
return self.added == other.added \
69
and self.removed == other.removed \
70
and self.renamed == other.renamed \
71
and self.modified == other.modified \
72
and self.unchanged == other.unchanged \
73
and self.kind_changed == other.kind_changed \
74
and self.unversioned == other.unversioned
70
and self.removed == other.removed \
71
and self.renamed == other.renamed \
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
76
77
def __ne__(self, other):
77
78
return not (self == other)
80
81
return "TreeDelta(added=%r, removed=%r, renamed=%r," \
81
82
" kind_changed=%r, modified=%r, unchanged=%r," \
82
83
" unversioned=%r)" % (self.added,
83
self.removed, self.renamed, self.kind_changed, self.modified,
84
self.unchanged, self.unversioned)
84
self.removed, self.renamed, self.kind_changed, self.modified,
85
self.unchanged, self.unversioned)
86
87
def has_changed(self):
87
88
return bool(self.modified
91
92
or self.kind_changed)
93
def touches_file_id(self, file_id):
94
"""Return True if file_id is modified by this delta."""
95
for l in self.added, self.removed, self.modified:
99
for v in self.renamed:
102
for v in self.kind_changed:
107
94
def get_changes_as_text(self, show_ids=False, show_unchanged=False,
108
95
short_status=False):
110
output = StringIO.StringIO()
111
97
report_delta(output, self, short_status, show_ids, show_unchanged)
112
98
return output.getvalue()
119
105
delta = TreeDelta()
120
106
# mutter('start compare_trees')
122
for (file_id, path, content_change, versioned, parent_id, name, kind,
123
executable) in new_tree.iter_changes(old_tree, want_unchanged,
124
specific_files, extra_trees=extra_trees,
108
for change in new_tree.iter_changes(
109
old_tree, want_unchanged, specific_files, extra_trees=extra_trees,
125
110
require_versioned=require_versioned,
126
111
want_unversioned=want_unversioned):
127
if versioned == (False, False):
128
delta.unversioned.append((path[1], None, kind[1]))
130
if not include_root and (None, None) == parent_id:
132
fully_present = tuple((versioned[x] and kind[x] is not None) for
112
if change.versioned == (False, False):
113
delta.unversioned.append(change)
115
if not include_root and (None, None) == change.parent_id:
117
fully_present = tuple(
118
(change.versioned[x] and change.kind[x] is not None)
134
120
if fully_present[0] != fully_present[1]:
135
121
if fully_present[1] is True:
136
delta.added.append((path[1], file_id, kind[1]))
122
delta.added.append(change)
138
delta.removed.append((path[0], file_id, kind[0]))
124
if change.kind[0] == 'symlink' and not new_tree.supports_symlinks():
126
'Ignoring "%s" as symlinks '
127
'are not supported on this filesystem.' % (change.path[0],))
129
delta.removed.append(change)
139
130
elif fully_present[0] is False:
141
elif name[0] != name[1] or parent_id[0] != parent_id[1]:
131
delta.missing.append(change)
132
elif change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]:
142
133
# If the name changes, or the parent_id changes, we have a rename
143
134
# (if we move a parent, that doesn't count as a rename for the
145
delta.renamed.append((path[0],
150
(executable[0] != executable[1])))
151
elif kind[0] != kind[1]:
152
delta.kind_changed.append((path[1], file_id, kind[0], kind[1]))
153
elif content_change or executable[0] != executable[1]:
154
delta.modified.append((path[1], file_id, kind[1],
156
(executable[0] != executable[1])))
158
delta.unchanged.append((path[1], file_id, kind[1]))
136
delta.renamed.append(change)
137
elif change.kind[0] != change.kind[1]:
138
delta.kind_changed.append(change)
139
elif change.changed_content or change.executable[0] != change.executable[1]:
140
delta.modified.append(change)
142
delta.unchanged.append(change)
144
def change_key(change):
145
if change.path[0] is None:
146
path = change.path[1]
148
path = change.path[0]
149
return (path, change.file_id)
151
delta.removed.sort(key=change_key)
152
delta.added.sort(key=change_key)
153
delta.renamed.sort(key=change_key)
154
delta.missing.sort(key=change_key)
163
155
# TODO: jam 20060529 These lists shouldn't need to be sorted
164
156
# since we added them in alphabetical order.
165
delta.modified.sort()
166
delta.unchanged.sort()
157
delta.modified.sort(key=change_key)
158
delta.unchanged.sort(key=change_key)
159
delta.unversioned.sort(key=change_key)
187
181
:param view_info: A tuple of view_name,view_files if only
188
182
items inside a view are to be reported on, or None for
189
183
no view filtering.
184
:param classify: Add special symbols to indicate file kind.
191
186
if output_file is not None:
192
187
if output is not None:
193
188
raise BzrError('Cannot specify both output and output_file')
194
190
def output(fmt, *args):
195
191
output_file.write((fmt % args) + '\n')
196
192
self.output = output
197
193
if self.output is None:
198
from bzrlib import trace
199
195
self.output = trace.note
200
196
self.suppress_root_add = suppress_root_add
201
197
self.modified_map = {'kind changed': 'K',
202
198
'unchanged': ' ',
206
self.versioned_map = {'added': '+', # versioned target
207
'unchanged': ' ', # versioned in both
208
'removed': '-', # versioned in source
209
'unversioned': '?', # versioned in neither
204
self.versioned_map = {'added': '+', # versioned target
205
'unchanged': ' ', # versioned in both
206
'removed': '-', # versioned in source
207
'unversioned': '?', # versioned in neither
211
209
self.unversioned_filter = unversioned_filter
211
self.kind_marker = osutils.kind_marker
213
self.kind_marker = lambda kind: ''
212
214
if view_info is None:
213
215
self.view_name = None
214
216
self.view_files = []
218
220
self.output("Operating on whole tree but only reporting on "
219
221
"'%s' view." % (self.view_name,))
221
def report(self, file_id, paths, versioned, renamed, modified, exe_change,
223
def report(self, paths, versioned, renamed, modified, exe_change,
223
225
"""Report one change to a file
225
:param file_id: The file_id of the file
226
227
:param path: The old and new paths as generated by Tree.iter_changes.
227
228
:param versioned: may be 'added', 'removed', 'unchanged', or
300
302
:param reporter: The _ChangeReporter that will report the changes.
302
304
versioned_change_map = {
303
(True, True) : 'unchanged',
304
(True, False) : 'removed',
305
(False, True) : 'added',
305
(True, True): 'unchanged',
306
(True, False): 'removed',
307
(False, True): 'added',
306
308
(False, False): 'unversioned',
308
for (file_id, path, content_change, versioned, parent_id, name, kind,
309
executable) in change_iterator:
311
def path_key(change):
312
if change.path[0] is not None:
313
path = change.path[0]
315
path = change.path[1]
316
return osutils.splitpath(path)
317
for change in sorted(change_iterator, key=path_key):
310
318
exe_change = False
311
319
# files are "renamed" if they are moved or if name changes, as long
312
320
# as it had a value
313
if None not in name and None not in parent_id and\
314
(name[0] != name[1] or parent_id[0] != parent_id[1]):
321
if None not in change.name and None not in change.parent_id and\
322
(change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]):
318
if kind[0] != kind[1]:
326
if change.kind[0] != change.kind[1]:
327
if change.kind[0] is None:
320
328
modified = "created"
321
elif kind[1] is None:
329
elif change.kind[1] is None:
322
330
modified = "deleted"
324
332
modified = "kind changed"
334
if change.changed_content:
327
335
modified = "modified"
336
elif change.kind[0] is None:
329
339
modified = "unchanged"
330
if kind[1] == "file":
331
exe_change = (executable[0] != executable[1])
332
versioned_change = versioned_change_map[versioned]
333
reporter.report(file_id, path, versioned_change, renamed, modified,
336
def report_delta(to_file, delta, short_status=False, show_ids=False,
337
show_unchanged=False, indent='', filter=None):
340
if change.kind[1] == "file":
341
exe_change = (change.executable[0] != change.executable[1])
342
versioned_change = versioned_change_map[change.versioned]
343
reporter.report(change.path, versioned_change, renamed, modified,
344
exe_change, change.kind)
347
def report_delta(to_file, delta, short_status=False, show_ids=False,
348
show_unchanged=False, indent='', predicate=None, classify=True):
338
349
"""Output this delta in status-like form to to_file.
340
351
:param to_file: A file-like object where the output is displayed.
366
381
def show_more_renamed(item):
367
(oldpath, file_id, kind,
368
text_modified, meta_modified, newpath) = item
369
dec_new_path = decorate_path(newpath, kind, meta_modified)
382
dec_new_path = decorate_path(item.path[1], item.kind[1], item.meta_modified())
370
383
to_file.write(' => %s' % dec_new_path)
371
if text_modified or meta_modified:
372
extra_modified.append((newpath, file_id, kind,
373
text_modified, meta_modified))
384
if item.changed_content or item.meta_modified():
385
extra_modified.append(TreeChange(
386
item.file_id, (item.path[1], item.path[1]),
387
item.changed_content,
389
(item.parent_id[1], item.parent_id[1]),
390
(item.name[1], item.name[1]),
391
(item.kind[1], item.kind[1]),
375
394
def show_more_kind_changed(item):
376
(path, file_id, old_kind, new_kind) = item
377
to_file.write(' (%s => %s)' % (old_kind, new_kind))
395
to_file.write(' (%s => %s)' % (item.kind[0], item.kind[1]))
379
def show_path(path, file_id, kind, meta_modified,
397
def show_path(path, kind, meta_modified,
380
398
default_format, with_file_id_format):
381
399
dec_path = decorate_path(path, kind, meta_modified)
396
414
prefix = indent + prefix + ' '
398
416
for item in files:
399
path, file_id, kind = item[:3]
400
if (filter is not None and not filter(path, file_id)):
417
if item.path[0] is None:
423
if predicate is not None and not predicate(path):
402
425
if not header_shown and not short_status:
403
426
to_file.write(indent + long_status_name + ':\n')
404
427
header_shown = True
407
meta_modified = item[4]
409
428
to_file.write(prefix)
410
show_path(path, file_id, kind, meta_modified,
429
show_path(path, kind, item.meta_modified(),
411
430
default_format, with_file_id_format)
412
431
if show_more is not None:
415
to_file.write(' %s' % file_id)
433
if show_ids and getattr(item, 'file_id', None):
434
to_file.write(' %s' % item.file_id.decode('utf-8'))
416
435
to_file.write('\n')
418
437
show_list(delta.removed, 'removed', 'D')
419
438
show_list(delta.added, 'added', 'A')
439
show_list(delta.missing, 'missing', '!')
420
440
extra_modified = []
421
# Reorder delta.renamed tuples so that all lists share the same
422
# order for their 3 first fields and that they also begin like
423
# the delta.modified tuples
424
renamed = [(p, i, k, tm, mm, np)
425
for p, np, i, k, tm, mm in delta.renamed]
426
show_list(renamed, 'renamed', 'R', with_file_id_format='%s',
441
show_list(delta.renamed, 'renamed', 'R', with_file_id_format='%s',
427
442
show_more=show_more_renamed)
428
443
show_list(delta.kind_changed, 'kind changed', 'K',
429
444
with_file_id_format='%s',