19
19
from errors import BzrError
22
# TODO: Rather than building a changeset object, we should probably
23
# invoke callbacks on an object. That object can either accumulate a
24
# list, write them out directly, etc etc.
22
26
def internal_diff(old_label, oldlines, new_label, newlines, to_file):
38
42
if not oldlines and not newlines:
43
if oldlines and (oldlines[-1][-1] != '\n'):
46
if newlines and (newlines[-1][-1] != '\n'):
50
45
ud = difflib.unified_diff(oldlines, newlines,
51
46
fromfile=old_label, tofile=new_label)
60
55
ud[2] = ud[2].replace('+1,0', '+0,0')
62
to_file.writelines(ud)
64
print >>to_file, "\\ No newline at end of file"
59
if not line.endswith('\n'):
60
to_file.write("\n\\ No newline at end of file\n")
70
def external_diff(old_label, oldlines, new_label, newlines, to_file):
66
def external_diff(old_label, oldlines, new_label, newlines, to_file,
71
68
"""Display a diff by calling out to the external diff program."""
75
72
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
75
# make sure our own output is properly ordered before the diff
78
78
from tempfile import NamedTemporaryFile
81
81
oldtmpf = NamedTemporaryFile()
82
82
newtmpf = NamedTemporaryFile()
98
system('diff -u --label %s %s --label %s %s' % (old_label, oldtmpf.name, new_label, newtmpf.name))
101
'--label', old_label,
103
'--label', new_label,
106
# diff only allows one style to be specified; they don't override.
107
# note that some of these take optargs, and the optargs can be
108
# directly appended to the options.
109
# this is only an approximate parser; it doesn't properly understand
111
for s in ['-c', '-u', '-C', '-U',
116
'-y', '--side-by-side',
128
diffcmd.extend(diff_opts)
130
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
132
if rc != 0 and rc != 1:
133
# returns 1 if files differ; that's OK
135
msg = 'signal %d' % (-rc)
137
msg = 'exit code %d' % rc
139
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
100
141
oldtmpf.close() # and delete
105
def diff_file(old_label, oldlines, new_label, newlines, to_file):
107
differ = external_diff
109
differ = internal_diff
111
differ(old_label, oldlines, new_label, newlines, to_file)
115
def show_diff(b, revision, specific_files):
146
def show_diff(b, revision, specific_files, external_diff_options=None):
147
"""Shortcut for showing the diff to the working tree.
153
None for each, or otherwise the old revision to compare against.
155
The more general form is show_diff_trees(), where the caller
156
supplies any two trees.
118
160
if revision == None:
123
165
new_tree = b.working_tree()
125
show_diff_trees(old_tree, new_tree, sys.stdout, specific_files)
129
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None):
167
show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
168
external_diff_options)
172
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
173
external_diff_options=None):
130
174
"""Show in text form the changes from one tree to another.
133
177
If set, include only changes to these files.
179
external_diff_options
180
If set, use an external GNU diff and pass these options.
136
183
# TODO: Options to control putting on a prefix or suffix, perhaps as a format string
145
192
# TODO: Generation of pseudo-diffs for added/deleted files could
146
193
# be usefully made into a much faster special case.
195
if external_diff_options:
196
assert isinstance(external_diff_options, basestring)
197
opts = external_diff_options.split()
198
def diff_file(olab, olines, nlab, nlines, to_file):
199
external_diff(olab, olines, nlab, nlines, to_file, opts)
201
diff_file = internal_diff
148
204
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
149
205
specific_files=specific_files)
151
207
for path, file_id, kind in delta.removed:
152
print '*** removed %s %r' % (kind, path)
208
print >>to_file, '*** removed %s %r' % (kind, path)
153
209
if kind == 'file':
154
210
diff_file(old_label + path,
155
211
old_tree.get_file(file_id).readlines(),
160
216
for path, file_id, kind in delta.added:
161
print '*** added %s %r' % (kind, path)
217
print >>to_file, '*** added %s %r' % (kind, path)
162
218
if kind == 'file':
163
219
diff_file(DEVNULL,
169
225
for old_path, new_path, file_id, kind, text_modified in delta.renamed:
170
print '*** renamed %s %r => %r' % (kind, old_path, new_path)
226
print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
171
227
if text_modified:
172
228
diff_file(old_label + old_path,
173
229
old_tree.get_file(file_id).readlines(),
178
234
for path, file_id, kind in delta.modified:
179
print '*** modified %s %r' % (kind, path)
235
print >>to_file, '*** modified %s %r' % (kind, path)
180
236
if kind == 'file':
181
237
diff_file(old_label + path,
182
238
old_tree.get_file(file_id).readlines(),
207
263
Files that are both modified and renamed are listed only in
208
264
renamed, with the text_modified flag true.
266
Files are only considered renamed if their name has changed or
267
their parent directory has changed. Renaming a directory
268
does not count as renaming all its contents.
210
270
The lists are normally sorted when the delta is created.
212
272
def __init__(self):
216
276
self.modified = []
217
277
self.unchanged = []
279
def __eq__(self, other):
280
if not isinstance(other, TreeDelta):
282
return self.added == other.added \
283
and self.removed == other.removed \
284
and self.renamed == other.renamed \
285
and self.modified == other.modified \
286
and self.unchanged == other.unchanged
288
def __ne__(self, other):
289
return not (self == other)
292
return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \
293
" unchanged=%r)" % (self.added, self.removed, self.renamed,
294
self.modified, self.unchanged)
296
def has_changed(self):
297
changes = len(self.added) + len(self.removed) + len(self.renamed)
298
changes += len(self.modified)
299
return (changes != 0)
220
301
def touches_file_id(self, file_id):
221
302
"""Return True if file_id is modified by this delta."""
271
def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None):
352
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
272
353
"""Describe changes from one tree to another.
274
355
Returns a TreeDelta with details of added, modified, renamed, and
300
381
for file_id in old_tree:
301
382
if file_id in new_tree:
302
kind = old_inv.get_file_kind(file_id)
303
assert kind == new_inv.get_file_kind(file_id)
383
old_ie = old_inv[file_id]
384
new_ie = new_inv[file_id]
387
assert kind == new_ie.kind
305
389
assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
306
390
'invalid file kind %r' % kind
308
392
if kind == 'root_directory':
311
old_path = old_inv.id2path(file_id)
312
new_path = new_inv.id2path(file_id)
314
395
if specific_files:
315
if (not is_inside_any(specific_files, old_path)
316
and not is_inside_any(specific_files, new_path)):
396
if (not is_inside_any(specific_files, old_inv.id2path(file_id))
397
and not is_inside_any(specific_files, new_inv.id2path(file_id))):
319
400
if kind == 'file':
329
410
# the same and the parents are unchanged all the way up.
330
411
# May not be worthwhile.
332
if old_path != new_path:
333
delta.renamed.append((old_path, new_path, file_id, kind,
413
if (old_ie.name != new_ie.name
414
or old_ie.parent_id != new_ie.parent_id):
415
delta.renamed.append((old_inv.id2path(file_id),
416
new_inv.id2path(file_id),
335
419
elif text_modified:
336
delta.modified.append((new_path, file_id, kind))
420
delta.modified.append((new_inv.id2path(file_id), file_id, kind))
337
421
elif want_unchanged:
338
delta.unchanged.append((new_path, file_id, kind))
422
delta.unchanged.append((new_inv.id2path(file_id), file_id, kind))
340
424
kind = old_inv.get_file_kind(file_id)
341
425
old_path = old_inv.id2path(file_id)