89
142
# regular named file (e.g. in the working directory) then we can
90
143
# compare directly to that, rather than copying it.
92
# TODO: Set the labels appropriately
94
145
oldtmpf.writelines(oldlines)
95
146
newtmpf.writelines(newlines)
100
system('diff -u --label %s %s --label %s %s' % (old_label, oldtmpf.name, new_label, newtmpf.name))
154
'--label', old_filename,
156
'--label', new_filename,
161
# diff only allows one style to be specified; they don't override.
162
# note that some of these take optargs, and the optargs can be
163
# directly appended to the options.
164
# this is only an approximate parser; it doesn't properly understand
166
for s in ['-c', '-u', '-C', '-U',
171
'-y', '--side-by-side',
183
diffcmd.extend(diff_opts)
185
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
186
out,err = pipe.communicate()
189
# internal_diff() adds a trailing newline, add one here for consistency
192
# 'diff' gives retcode == 2 for all sorts of errors
193
# one of those is 'Binary files differ'.
194
# Bad options could also be the problem.
195
# 'Binary files' is not a real error, so we suppress that error.
198
# Since we got here, we want to make sure to give an i18n error
199
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
200
out, err = pipe.communicate()
202
# Write out the new i18n diff response
203
to_file.write(out+'\n')
204
if pipe.returncode != 2:
205
raise BzrError('external diff failed with exit code 2'
206
' when run with LANG=C, but not when run'
207
' natively: %r' % (diffcmd,))
209
first_line = lang_c_out.split('\n', 1)[0]
210
# Starting with diffutils 2.8.4 the word "binary" was dropped.
211
m = re.match('^(binary )?files.*differ$', first_line, re.I)
213
raise BzrError('external diff failed with exit code 2;'
214
' command: %r' % (diffcmd,))
216
# Binary files differ, just return
219
# If we got to here, we haven't written out the output of diff
223
# returns 1 if files differ; that's OK
225
msg = 'signal %d' % (-rc)
227
msg = 'exit code %d' % rc
229
raise BzrError('external diff failed with %s; command: %r'
102
234
oldtmpf.close() # and delete
107
def diff_file(old_label, oldlines, new_label, newlines, to_file):
109
differ = external_diff
111
differ = internal_diff
113
differ(old_label, oldlines, new_label, newlines, to_file)
117
def show_diff(b, revision, specific_files):
121
old_tree = b.basis_tree()
123
old_tree = b.revision_tree(b.lookup_revision(revision))
125
new_tree = b.working_tree()
127
show_diff_trees(old_tree, new_tree, sys.stdout, specific_files)
131
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None):
236
# Clean up. Warn in case the files couldn't be deleted
237
# (in case windows still holds the file open, but not
238
# if the files have already been deleted)
240
os.remove(old_abspath)
242
if e.errno not in (errno.ENOENT,):
243
warning('Failed to delete temporary file: %s %s',
246
os.remove(new_abspath)
248
if e.errno not in (errno.ENOENT,):
249
warning('Failed to delete temporary file: %s %s',
253
@deprecated_function(zero_eight)
254
def show_diff(b, from_spec, specific_files, external_diff_options=None,
255
revision2=None, output=None, b2=None):
256
"""Shortcut for showing the diff to the working tree.
258
Please use show_diff_trees instead.
264
None for 'basis tree', or otherwise the old revision to compare against.
266
The more general form is show_diff_trees(), where the caller
267
supplies any two trees.
272
if from_spec is None:
273
old_tree = b.bzrdir.open_workingtree()
275
old_tree = old_tree = old_tree.basis_tree()
277
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
279
if revision2 is None:
281
new_tree = b.bzrdir.open_workingtree()
283
new_tree = b2.bzrdir.open_workingtree()
285
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
287
return show_diff_trees(old_tree, new_tree, output, specific_files,
288
external_diff_options)
291
def diff_cmd_helper(tree, specific_files, external_diff_options,
292
old_revision_spec=None, new_revision_spec=None,
293
old_label='a/', new_label='b/'):
294
"""Helper for cmd_diff.
300
The specific files to compare, or None
302
external_diff_options
303
If non-None, run an external diff, and pass it these options
306
If None, use basis tree as old revision, otherwise use the tree for
307
the specified revision.
310
If None, use working tree as new revision, otherwise use the tree for
311
the specified revision.
313
The more general form is show_diff_trees(), where the caller
314
supplies any two trees.
318
revision = spec.in_store(tree.branch)
320
revision = spec.in_store(None)
321
revision_id = revision.rev_id
322
branch = revision.branch
323
return branch.repository.revision_tree(revision_id)
324
if old_revision_spec is None:
325
old_tree = tree.basis_tree()
327
old_tree = spec_tree(old_revision_spec)
329
if new_revision_spec is None:
332
new_tree = spec_tree(new_revision_spec)
333
if new_tree is not tree:
334
extra_trees = (tree,)
338
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
339
external_diff_options,
340
old_label=old_label, new_label=new_label,
341
extra_trees=extra_trees)
344
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
345
external_diff_options=None,
346
old_label='a/', new_label='b/',
132
348
"""Show in text form the changes from one tree to another.
135
351
If set, include only changes to these files.
353
external_diff_options
354
If set, use an external GNU diff and pass these options.
357
If set, more Trees to use for looking up file ids
138
# TODO: Options to control putting on a prefix or suffix, perhaps as a format string
142
DEVNULL = '/dev/null'
143
# Windows users, don't panic about this filename -- it is a
144
# special signal to GNU patch that the file should be created or
145
# deleted respectively.
363
return _show_diff_trees(old_tree, new_tree, to_file,
364
specific_files, external_diff_options,
365
old_label=old_label, new_label=new_label,
366
extra_trees=extra_trees)
373
def _show_diff_trees(old_tree, new_tree, to_file,
374
specific_files, external_diff_options,
375
old_label='a/', new_label='b/', extra_trees=None):
377
# GNU Patch uses the epoch date to detect files that are being added
378
# or removed in a diff.
379
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
147
381
# TODO: Generation of pseudo-diffs for added/deleted files could
148
382
# be usefully made into a much faster special case.
150
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
151
specific_files=specific_files)
384
if external_diff_options:
385
assert isinstance(external_diff_options, basestring)
386
opts = external_diff_options.split()
387
def diff_file(olab, olines, nlab, nlines, to_file):
388
external_diff(olab, olines, nlab, nlines, to_file, opts)
390
diff_file = internal_diff
392
delta = new_tree.changes_from(old_tree,
393
specific_files=specific_files,
394
extra_trees=extra_trees, require_versioned=True)
153
397
for path, file_id, kind in delta.removed:
154
print '*** removed %s %r' % (kind, path)
156
diff_file(old_label + path,
157
old_tree.get_file(file_id).readlines(),
399
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
400
old_name = '%s%s\t%s' % (old_label, path,
401
_patch_header_date(old_tree, file_id, path))
402
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
403
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
404
new_name, None, None, to_file)
162
405
for path, file_id, kind in delta.added:
163
print '*** added %s %r' % (kind, path)
168
new_tree.get_file(file_id).readlines(),
171
for old_path, new_path, file_id, kind, text_modified in delta.renamed:
172
print '*** renamed %s %r => %r' % (kind, old_path, new_path)
407
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
408
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
409
new_name = '%s%s\t%s' % (new_label, path,
410
_patch_header_date(new_tree, file_id, path))
411
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
412
old_name, None, None, to_file,
414
for (old_path, new_path, file_id, kind,
415
text_modified, meta_modified) in delta.renamed:
417
prop_str = get_prop_change(meta_modified)
418
print >>to_file, '=== renamed %s %r => %r%s' % (
419
kind, old_path.encode('utf8'),
420
new_path.encode('utf8'), prop_str)
421
old_name = '%s%s\t%s' % (old_label, old_path,
422
_patch_header_date(old_tree, file_id,
424
new_name = '%s%s\t%s' % (new_label, new_path,
425
_patch_header_date(new_tree, file_id,
427
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
429
text_modified, kind, to_file, diff_file)
430
for path, file_id, kind, text_modified, meta_modified in delta.modified:
432
prop_str = get_prop_change(meta_modified)
433
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
434
old_name = '%s%s\t%s' % (old_label, path,
435
_patch_header_date(old_tree, file_id, path))
436
new_name = '%s%s\t%s' % (new_label, path,
437
_patch_header_date(new_tree, file_id, path))
173
438
if text_modified:
174
diff_file(old_label + old_path,
175
old_tree.get_file(file_id).readlines(),
176
new_label + new_path,
177
new_tree.get_file(file_id).readlines(),
180
for path, file_id, kind in delta.modified:
181
print '*** modified %s %r' % (kind, path)
183
diff_file(old_label + path,
184
old_tree.get_file(file_id).readlines(),
186
new_tree.get_file(file_id).readlines(),
191
class TreeDelta(object):
192
"""Describes changes from one tree to another.
201
(oldpath, newpath, id, kind, text_modified)
207
Each id is listed only once.
209
Files that are both modified and renamed are listed only in
210
renamed, with the text_modified flag true.
212
The lists are normally sorted when the delta is created.
222
def touches_file_id(self, file_id):
223
"""Return True if file_id is modified by this delta."""
224
for l in self.added, self.removed, self.modified:
228
for v in self.renamed:
234
def show(self, to_file, show_ids=False, show_unchanged=False):
235
def show_list(files):
236
for path, fid, kind in files:
237
if kind == 'directory':
239
elif kind == 'symlink':
243
print >>to_file, ' %-30s %s' % (path, fid)
245
print >>to_file, ' ', path
248
print >>to_file, 'removed:'
249
show_list(self.removed)
252
print >>to_file, 'added:'
253
show_list(self.added)
256
print >>to_file, 'renamed:'
257
for oldpath, newpath, fid, kind, text_modified in self.renamed:
259
print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid)
261
print >>to_file, ' %s => %s' % (oldpath, newpath)
264
print >>to_file, 'modified:'
265
show_list(self.modified)
267
if show_unchanged and self.unchanged:
268
print >>to_file, 'unchanged:'
269
show_list(self.unchanged)
273
def compare_trees(old_tree, new_tree, want_unchanged, specific_files=None):
274
"""Describe changes from one tree to another.
276
Returns a TreeDelta with details of added, modified, renamed, and
279
The root entry is specifically exempt.
281
This only considers versioned files.
284
If true, also list files unchanged from one version to
288
If true, only check for changes to specified names or
292
from osutils import is_inside_any
439
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
441
True, kind, to_file, diff_file)
446
def _patch_header_date(tree, file_id, path):
447
"""Returns a timestamp suitable for use in a patch header."""
448
tm = time.gmtime(tree.get_file_mtime(file_id, path))
449
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
452
def _raise_if_nonexistent(paths, old_tree, new_tree):
453
"""Complain if paths are not in either inventory or tree.
455
It's OK with the files exist in either tree's inventory, or
456
if they exist in the tree but are not versioned.
294
old_inv = old_tree.inventory
295
new_inv = new_tree.inventory
297
mutter('start compare_trees')
299
# TODO: match for specific files can be rather smarter by finding
300
# the IDs of those files up front and then considering only that.
302
for file_id in old_tree:
303
if file_id in new_tree:
304
kind = old_inv.get_file_kind(file_id)
305
assert kind == new_inv.get_file_kind(file_id)
307
assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
308
'invalid file kind %r' % kind
310
if kind == 'root_directory':
313
old_path = old_inv.id2path(file_id)
314
new_path = new_inv.id2path(file_id)
317
if (not is_inside_any(specific_files, old_path)
318
and not is_inside_any(specific_files, new_path)):
322
old_sha1 = old_tree.get_file_sha1(file_id)
323
new_sha1 = new_tree.get_file_sha1(file_id)
324
text_modified = (old_sha1 != new_sha1)
326
## mutter("no text to check for %r %r" % (file_id, kind))
327
text_modified = False
329
# TODO: Can possibly avoid calculating path strings if the
330
# two files are unchanged and their names and parents are
331
# the same and the parents are unchanged all the way up.
332
# May not be worthwhile.
334
if old_path != new_path:
335
delta.renamed.append((old_path, new_path, file_id, kind,
338
delta.modified.append((new_path, file_id, kind))
340
delta.unchanged.append((new_path, file_id, kind))
342
kind = old_inv.get_file_kind(file_id)
343
old_path = old_inv.id2path(file_id)
345
if not is_inside_any(specific_files, old_path):
347
delta.removed.append((old_path, file_id, kind))
349
mutter('start looking for new files')
350
for file_id in new_inv:
351
if file_id in old_inv:
353
new_path = new_inv.id2path(file_id)
355
if not is_inside_any(specific_files, new_path):
357
kind = new_inv.get_file_kind(file_id)
358
delta.added.append((new_path, file_id, kind))
363
delta.modified.sort()
364
delta.unchanged.sort()
458
This can be used by operations such as bzr status that can accept
459
unknown or ignored files.
461
mutter("check paths: %r", paths)
464
s = old_tree.filter_unversioned_files(paths)
465
s = new_tree.filter_unversioned_files(s)
466
s = [path for path in s if not new_tree.has_filename(path)]
468
raise errors.PathsDoNotExist(sorted(s))
471
def get_prop_change(meta_modified):
473
return " (properties changed)"
478
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
479
new_path, new_tree, text_modified,
480
kind, to_file, diff_file):
482
new_entry = new_tree.inventory[file_id]
483
old_tree.inventory[file_id].diff(diff_file,