157
89
# regular named file (e.g. in the working directory) then we can
158
90
# compare directly to that, rather than copying it.
92
# TODO: Set the labels appropriately
160
94
oldtmpf.writelines(oldlines)
161
95
newtmpf.writelines(newlines)
100
system('diff -u --label %s %s --label %s %s' % (old_label, oldtmpf.name, new_label, newtmpf.name))
102
oldtmpf.close() # and delete
169
'--label', old_filename,
171
'--label', new_filename,
176
# diff only allows one style to be specified; they don't override.
177
# note that some of these take optargs, and the optargs can be
178
# directly appended to the options.
179
# this is only an approximate parser; it doesn't properly understand
181
for s in ['-c', '-u', '-C', '-U',
186
'-y', '--side-by-side',
198
diffcmd.extend(diff_opts)
200
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
201
out,err = pipe.communicate()
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))
204
# internal_diff() adds a trailing newline, add one here for consistency
207
# 'diff' gives retcode == 2 for all sorts of errors
208
# one of those is 'Binary files differ'.
209
# Bad options could also be the problem.
210
# 'Binary files' is not a real error, so we suppress that error.
213
# Since we got here, we want to make sure to give an i18n error
214
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
215
out, err = pipe.communicate()
217
# Write out the new i18n diff response
218
to_file.write(out+'\n')
219
if pipe.returncode != 2:
220
raise errors.BzrError(
221
'external diff failed with exit code 2'
222
' when run with LANG=C and LC_ALL=C,'
223
' but not when run natively: %r' % (diffcmd,))
225
first_line = lang_c_out.split('\n', 1)[0]
226
# Starting with diffutils 2.8.4 the word "binary" was dropped.
227
m = re.match('^(binary )?files.*differ$', first_line, re.I)
229
raise errors.BzrError('external diff failed with exit code 2;'
230
' command: %r' % (diffcmd,))
232
# Binary files differ, just return
235
# If we got to here, we haven't written out the output of diff
239
# returns 1 if files differ; that's OK
241
msg = 'signal %d' % (-rc)
243
msg = 'exit code %d' % rc
245
raise errors.BzrError('external diff failed with %s; command: %r'
250
oldtmpf.close() # and delete
252
# Clean up. Warn in case the files couldn't be deleted
253
# (in case windows still holds the file open, but not
254
# if the files have already been deleted)
256
os.remove(old_abspath)
258
if e.errno not in (errno.ENOENT,):
259
warning('Failed to delete temporary file: %s %s',
262
os.remove(new_abspath)
264
if e.errno not in (errno.ENOENT,):
265
warning('Failed to delete temporary file: %s %s',
269
@deprecated_function(zero_eight)
270
def show_diff(b, from_spec, specific_files, external_diff_options=None,
271
revision2=None, output=None, b2=None):
272
"""Shortcut for showing the diff to the working tree.
274
Please use show_diff_trees instead.
280
None for 'basis tree', or otherwise the old revision to compare against.
282
The more general form is show_diff_trees(), where the caller
283
supplies any two trees.
288
if from_spec is None:
289
old_tree = b.bzrdir.open_workingtree()
291
old_tree = old_tree = old_tree.basis_tree()
293
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
295
if revision2 is None:
297
new_tree = b.bzrdir.open_workingtree()
299
new_tree = b2.bzrdir.open_workingtree()
301
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
303
return show_diff_trees(old_tree, new_tree, output, specific_files,
304
external_diff_options)
307
def diff_cmd_helper(tree, specific_files, external_diff_options,
308
old_revision_spec=None, new_revision_spec=None,
310
old_label='a/', new_label='b/'):
311
"""Helper for cmd_diff.
316
:param specific_files:
317
The specific files to compare, or None
319
:param external_diff_options:
320
If non-None, run an external diff, and pass it these options
322
:param old_revision_spec:
323
If None, use basis tree as old revision, otherwise use the tree for
324
the specified revision.
326
:param new_revision_spec:
327
If None, use working tree as new revision, otherwise use the tree for
328
the specified revision.
330
:param revision_specs:
331
Zero, one or two RevisionSpecs from the command line, saying what revisions
332
to compare. This can be passed as an alternative to the old_revision_spec
333
and new_revision_spec parameters.
335
The more general form is show_diff_trees(), where the caller
336
supplies any two trees.
339
# TODO: perhaps remove the old parameters old_revision_spec and
340
# new_revision_spec, since this is only really for use from cmd_diff and
341
# it now always passes through a sequence of revision_specs -- mbp
346
revision = spec.in_store(tree.branch)
348
revision = spec.in_store(None)
349
revision_id = revision.rev_id
350
branch = revision.branch
351
return branch.repository.revision_tree(revision_id)
353
if revision_specs is not None:
354
assert (old_revision_spec is None
355
and new_revision_spec is None)
356
if len(revision_specs) > 0:
357
old_revision_spec = revision_specs[0]
358
if len(revision_specs) > 1:
359
new_revision_spec = revision_specs[1]
361
if old_revision_spec is None:
362
old_tree = tree.basis_tree()
364
old_tree = spec_tree(old_revision_spec)
366
if (new_revision_spec is None
367
or new_revision_spec.spec is None):
370
new_tree = spec_tree(new_revision_spec)
372
if new_tree is not tree:
373
extra_trees = (tree,)
377
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
378
external_diff_options,
379
old_label=old_label, new_label=new_label,
380
extra_trees=extra_trees)
383
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
384
external_diff_options=None,
385
old_label='a/', new_label='b/',
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):
387
132
"""Show in text form the changes from one tree to another.
390
135
If set, include only changes to these files.
392
external_diff_options
393
If set, use an external GNU diff and pass these options.
396
If set, more Trees to use for looking up file ids
400
if extra_trees is not None:
401
for tree in extra_trees:
405
return _show_diff_trees(old_tree, new_tree, to_file,
406
specific_files, external_diff_options,
407
old_label=old_label, new_label=new_label,
408
extra_trees=extra_trees)
411
if extra_trees is not None:
412
for tree in extra_trees:
418
def _show_diff_trees(old_tree, new_tree, to_file,
419
specific_files, external_diff_options,
420
old_label='a/', new_label='b/', extra_trees=None):
422
# GNU Patch uses the epoch date to detect files that are being added
423
# or removed in a diff.
424
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
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.
426
147
# TODO: Generation of pseudo-diffs for added/deleted files could
427
148
# be usefully made into a much faster special case.
429
if external_diff_options:
430
assert isinstance(external_diff_options, basestring)
431
opts = external_diff_options.split()
432
def diff_file(olab, olines, nlab, nlines, to_file):
433
external_diff(olab, olines, nlab, nlines, to_file, opts)
435
diff_file = internal_diff
437
delta = new_tree.changes_from(old_tree,
438
specific_files=specific_files,
439
extra_trees=extra_trees, require_versioned=True)
150
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
151
specific_files=specific_files)
442
153
for path, file_id, kind in delta.removed:
444
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
445
old_name = '%s%s\t%s' % (old_label, path,
446
_patch_header_date(old_tree, file_id, path))
447
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
448
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
449
new_name, None, None, to_file)
154
print '*** removed %s %r' % (kind, path)
156
diff_file(old_label + path,
157
old_tree.get_file(file_id).readlines(),
450
162
for path, file_id, kind in delta.added:
452
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
453
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
454
new_name = '%s%s\t%s' % (new_label, path,
455
_patch_header_date(new_tree, file_id, path))
456
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
457
old_name, None, None, to_file,
459
for (old_path, new_path, file_id, kind,
460
text_modified, meta_modified) in delta.renamed:
462
prop_str = get_prop_change(meta_modified)
463
print >>to_file, '=== renamed %s %r => %r%s' % (
464
kind, old_path.encode('utf8'),
465
new_path.encode('utf8'), prop_str)
466
old_name = '%s%s\t%s' % (old_label, old_path,
467
_patch_header_date(old_tree, file_id,
469
new_name = '%s%s\t%s' % (new_label, new_path,
470
_patch_header_date(new_tree, file_id,
472
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
474
text_modified, kind, to_file, diff_file)
475
for path, file_id, kind, text_modified, meta_modified in delta.modified:
477
prop_str = get_prop_change(meta_modified)
478
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
479
old_name = '%s%s\t%s' % (old_label, path,
480
_patch_header_date(old_tree, file_id, path))
481
new_name = '%s%s\t%s' % (new_label, path,
482
_patch_header_date(new_tree, file_id, path))
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)
483
173
if text_modified:
484
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
486
True, kind, to_file, diff_file)
491
def _patch_header_date(tree, file_id, path):
492
"""Returns a timestamp suitable for use in a patch header."""
493
tm = time.gmtime(tree.get_file_mtime(file_id, path))
494
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
497
def _raise_if_nonexistent(paths, old_tree, new_tree):
498
"""Complain if paths are not in either inventory or tree.
500
It's OK with the files exist in either tree's inventory, or
501
if they exist in the tree but are not versioned.
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
503
This can be used by operations such as bzr status that can accept
504
unknown or ignored files.
506
mutter("check paths: %r", paths)
509
s = old_tree.filter_unversioned_files(paths)
510
s = new_tree.filter_unversioned_files(s)
511
s = [path for path in s if not new_tree.has_filename(path)]
513
raise errors.PathsDoNotExist(sorted(s))
516
def get_prop_change(meta_modified):
518
return " (properties changed)"
523
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
524
new_path, new_tree, text_modified,
525
kind, to_file, diff_file):
527
new_entry = new_tree.inventory[file_id]
528
old_tree.inventory[file_id].diff(diff_file,
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()