60
41
# both sequences are empty.
61
42
if not oldlines and not newlines:
64
if allow_binary is False:
65
check_text_lines(oldlines)
66
check_text_lines(newlines)
68
if sequence_matcher is None:
69
sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
70
ud = unified_diff(oldlines, newlines,
71
fromfile=old_filename.encode(path_encoding),
72
tofile=new_filename.encode(path_encoding),
73
sequencematcher=sequence_matcher)
47
if oldlines and (oldlines[-1][-1] != '\n'):
50
if newlines and (newlines[-1][-1] != '\n'):
54
ud = difflib.unified_diff(oldlines, newlines,
55
fromfile=old_label, tofile=new_label)
76
57
# work-around for difflib being too smart for its own good
77
58
# if /dev/null is "1,0", patch won't recognize it as /dev/null
79
61
ud[2] = ud[2].replace('-1,0', '-0,0')
81
64
ud[2] = ud[2].replace('+1,0', '+0,0')
82
# work around for difflib emitting random spaces after the label
83
ud[0] = ud[0][:-2] + '\n'
84
ud[1] = ud[1][:-2] + '\n'
88
if not line.endswith('\n'):
89
to_file.write("\n\\ No newline at end of file\n")
66
to_file.writelines(ud)
68
print >>to_file, "\\ No newline at end of file"
94
"""Set the env var LANG=C"""
95
osutils.set_or_unset_env('LANG', 'C')
96
osutils.set_or_unset_env('LC_ALL', None)
97
osutils.set_or_unset_env('LC_CTYPE', None)
98
osutils.set_or_unset_env('LANGUAGE', None)
101
def _spawn_external_diff(diffcmd, capture_errors=True):
102
"""Spawn the externall diff process, and return the child handle.
104
:param diffcmd: The command list to spawn
105
:param capture_errors: Capture stderr as well as setting LANG=C.
106
This lets us read and understand the output of diff, and respond
108
:return: A Popen object.
111
if sys.platform == 'win32':
112
# Win32 doesn't support preexec_fn, but that is
113
# okay, because it doesn't support LANG either.
116
preexec_fn = _set_lang_C
117
stderr = subprocess.PIPE
123
pipe = subprocess.Popen(diffcmd,
124
stdin=subprocess.PIPE,
125
stdout=subprocess.PIPE,
127
preexec_fn=preexec_fn)
129
if e.errno == errno.ENOENT:
130
raise errors.NoDiff(str(e))
136
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
74
def external_diff(old_label, oldlines, new_label, newlines, to_file,
138
76
"""Display a diff by calling out to the external diff program."""
79
if to_file != sys.stdout:
80
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
139
83
# make sure our own output is properly ordered before the diff
142
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
143
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
144
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
145
newtmpf = os.fdopen(newtmp_fd, 'wb')
86
from tempfile import NamedTemporaryFile
89
oldtmpf = NamedTemporaryFile()
90
newtmpf = NamedTemporaryFile()
148
93
# TODO: perhaps a special case for comparing to or from the empty
193
136
diffcmd.extend(diff_opts)
195
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
196
out,err = pipe.communicate()
138
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
199
# internal_diff() adds a trailing newline, add one here for consistency
202
# 'diff' gives retcode == 2 for all sorts of errors
203
# one of those is 'Binary files differ'.
204
# Bad options could also be the problem.
205
# 'Binary files' is not a real error, so we suppress that error.
208
# Since we got here, we want to make sure to give an i18n error
209
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
210
out, err = pipe.communicate()
212
# Write out the new i18n diff response
213
to_file.write(out+'\n')
214
if pipe.returncode != 2:
215
raise BzrError('external diff failed with exit code 2'
216
' when run with LANG=C, but not when run'
217
' natively: %r' % (diffcmd,))
219
first_line = lang_c_out.split('\n', 1)[0]
220
# Starting with diffutils 2.8.4 the word "binary" was dropped.
221
m = re.match('^(binary )?files.*differ$', first_line, re.I)
223
raise BzrError('external diff failed with exit code 2;'
224
' command: %r' % (diffcmd,))
226
# Binary files differ, just return
229
# If we got to here, we haven't written out the output of diff
140
if rc != 0 and rc != 1:
233
141
# returns 1 if files differ; that's OK
235
143
msg = 'signal %d' % (-rc)
237
145
msg = 'exit code %d' % rc
239
raise BzrError('external diff failed with %s; command: %r'
147
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
244
149
oldtmpf.close() # and delete
246
# Clean up. Warn in case the files couldn't be deleted
247
# (in case windows still holds the file open, but not
248
# if the files have already been deleted)
250
os.remove(old_abspath)
252
if e.errno not in (errno.ENOENT,):
253
warning('Failed to delete temporary file: %s %s',
256
os.remove(new_abspath)
258
if e.errno not in (errno.ENOENT,):
259
warning('Failed to delete temporary file: %s %s',
263
@deprecated_function(zero_eight)
264
def show_diff(b, from_spec, specific_files, external_diff_options=None,
265
revision2=None, output=None, b2=None):
154
def show_diff(b, revision, specific_files, external_diff_options=None):
266
155
"""Shortcut for showing the diff to the working tree.
268
Please use show_diff_trees instead.
274
None for 'basis tree', or otherwise the old revision to compare against.
276
The more general form is show_diff_trees(), where the caller
277
supplies any two trees.
282
if from_spec is None:
283
old_tree = b.bzrdir.open_workingtree()
285
old_tree = old_tree = old_tree.basis_tree()
287
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
289
if revision2 is None:
291
new_tree = b.bzrdir.open_workingtree()
293
new_tree = b2.bzrdir.open_workingtree()
295
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
297
return show_diff_trees(old_tree, new_tree, output, specific_files,
298
external_diff_options)
301
def diff_cmd_helper(tree, specific_files, external_diff_options,
302
old_revision_spec=None, new_revision_spec=None,
303
old_label='a/', new_label='b/'):
304
"""Helper for cmd_diff.
310
The specific files to compare, or None
312
external_diff_options
313
If non-None, run an external diff, and pass it these options
316
If None, use basis tree as old revision, otherwise use the tree for
317
the specified revision.
320
If None, use working tree as new revision, otherwise use the tree for
321
the specified revision.
323
The more general form is show_diff_trees(), where the caller
324
supplies any two trees.
328
revision = spec.in_store(tree.branch)
330
revision = spec.in_store(None)
331
revision_id = revision.rev_id
332
branch = revision.branch
333
return branch.repository.revision_tree(revision_id)
334
if old_revision_spec is None:
335
old_tree = tree.basis_tree()
337
old_tree = spec_tree(old_revision_spec)
339
if new_revision_spec is None:
342
new_tree = spec_tree(new_revision_spec)
343
if new_tree is not tree:
344
extra_trees = (tree,)
348
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
349
external_diff_options,
350
old_label=old_label, new_label=new_label,
351
extra_trees=extra_trees)
161
None for each, or otherwise the old revision to compare against.
163
The more general form is show_diff_trees(), where the caller
164
supplies any two trees.
169
old_tree = b.basis_tree()
171
old_tree = b.revision_tree(b.lookup_revision(revision))
173
new_tree = b.working_tree()
175
show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
176
external_diff_options)
354
180
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
355
external_diff_options=None,
356
old_label='a/', new_label='b/',
181
external_diff_options=None):
358
182
"""Show in text form the changes from one tree to another.
400
209
diff_file = internal_diff
402
delta = new_tree.changes_from(old_tree,
403
specific_files=specific_files,
404
extra_trees=extra_trees, require_versioned=True)
212
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
213
specific_files=specific_files)
407
215
for path, file_id, kind in delta.removed:
409
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
410
old_name = '%s%s\t%s' % (old_label, path,
411
_patch_header_date(old_tree, file_id, path))
412
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
413
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
414
new_name, None, None, to_file)
216
print >>to_file, '*** removed %s %r' % (kind, path)
218
diff_file(old_label + path,
219
old_tree.get_file(file_id).readlines(),
415
224
for path, file_id, kind in delta.added:
417
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
418
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
419
new_name = '%s%s\t%s' % (new_label, path,
420
_patch_header_date(new_tree, file_id, path))
421
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
422
old_name, None, None, to_file,
424
for (old_path, new_path, file_id, kind,
425
text_modified, meta_modified) in delta.renamed:
427
prop_str = get_prop_change(meta_modified)
428
print >>to_file, '=== renamed %s %r => %r%s' % (
429
kind, old_path.encode('utf8'),
430
new_path.encode('utf8'), prop_str)
431
old_name = '%s%s\t%s' % (old_label, old_path,
432
_patch_header_date(old_tree, file_id,
434
new_name = '%s%s\t%s' % (new_label, new_path,
435
_patch_header_date(new_tree, file_id,
437
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
439
text_modified, kind, to_file, diff_file)
440
for path, file_id, kind, text_modified, meta_modified in delta.modified:
442
prop_str = get_prop_change(meta_modified)
443
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
444
old_name = '%s%s\t%s' % (old_label, path,
445
_patch_header_date(old_tree, file_id, path))
446
new_name = '%s%s\t%s' % (new_label, path,
447
_patch_header_date(new_tree, file_id, path))
225
print >>to_file, '*** added %s %r' % (kind, path)
230
new_tree.get_file(file_id).readlines(),
233
for old_path, new_path, file_id, kind, text_modified in delta.renamed:
234
print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
448
235
if text_modified:
449
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
451
True, kind, to_file, diff_file)
456
def _patch_header_date(tree, file_id, path):
457
"""Returns a timestamp suitable for use in a patch header."""
458
tm = time.gmtime(tree.get_file_mtime(file_id, path))
459
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
462
def _raise_if_nonexistent(paths, old_tree, new_tree):
463
"""Complain if paths are not in either inventory or tree.
465
It's OK with the files exist in either tree's inventory, or
466
if they exist in the tree but are not versioned.
236
diff_file(old_label + old_path,
237
old_tree.get_file(file_id).readlines(),
238
new_label + new_path,
239
new_tree.get_file(file_id).readlines(),
242
for path, file_id, kind in delta.modified:
243
print >>to_file, '*** modified %s %r' % (kind, path)
245
diff_file(old_label + path,
246
old_tree.get_file(file_id).readlines(),
248
new_tree.get_file(file_id).readlines(),
253
class TreeDelta(object):
254
"""Describes changes from one tree to another.
263
(oldpath, newpath, id, kind, text_modified)
269
Each id is listed only once.
271
Files that are both modified and renamed are listed only in
272
renamed, with the text_modified flag true.
274
Files are only considered renamed if their name has changed or
275
their parent directory has changed. Renaming a directory
276
does not count as renaming all its contents.
278
The lists are normally sorted when the delta is created.
287
def __eq__(self, other):
288
if not isinstance(other, TreeDelta):
290
return self.added == other.added \
291
and self.removed == other.removed \
292
and self.renamed == other.renamed \
293
and self.modified == other.modified \
294
and self.unchanged == other.unchanged
296
def __ne__(self, other):
297
return not (self == other)
300
return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \
301
" unchanged=%r)" % (self.added, self.removed, self.renamed,
302
self.modified, self.unchanged)
304
def has_changed(self):
305
changes = len(self.added) + len(self.removed) + len(self.renamed)
306
changes += len(self.modified)
307
return (changes != 0)
309
def touches_file_id(self, file_id):
310
"""Return True if file_id is modified by this delta."""
311
for l in self.added, self.removed, self.modified:
315
for v in self.renamed:
321
def show(self, to_file, show_ids=False, show_unchanged=False):
322
def show_list(files):
323
for path, fid, kind in files:
324
if kind == 'directory':
326
elif kind == 'symlink':
330
print >>to_file, ' %-30s %s' % (path, fid)
332
print >>to_file, ' ', path
335
print >>to_file, 'removed:'
336
show_list(self.removed)
339
print >>to_file, 'added:'
340
show_list(self.added)
343
print >>to_file, 'renamed:'
344
for oldpath, newpath, fid, kind, text_modified in self.renamed:
346
print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid)
348
print >>to_file, ' %s => %s' % (oldpath, newpath)
351
print >>to_file, 'modified:'
352
show_list(self.modified)
354
if show_unchanged and self.unchanged:
355
print >>to_file, 'unchanged:'
356
show_list(self.unchanged)
360
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
361
"""Describe changes from one tree to another.
363
Returns a TreeDelta with details of added, modified, renamed, and
366
The root entry is specifically exempt.
368
This only considers versioned files.
371
If true, also list files unchanged from one version to
375
If true, only check for changes to specified names or
379
from osutils import is_inside_any
468
This can be used by operations such as bzr status that can accept
469
unknown or ignored files.
471
mutter("check paths: %r", paths)
474
s = old_tree.filter_unversioned_files(paths)
475
s = new_tree.filter_unversioned_files(s)
476
s = [path for path in s if not new_tree.has_filename(path)]
478
raise errors.PathsDoNotExist(sorted(s))
481
def get_prop_change(meta_modified):
483
return " (properties changed)"
488
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
489
new_path, new_tree, text_modified,
490
kind, to_file, diff_file):
492
new_entry = new_tree.inventory[file_id]
493
old_tree.inventory[file_id].diff(diff_file,
381
old_inv = old_tree.inventory
382
new_inv = new_tree.inventory
384
mutter('start compare_trees')
386
# TODO: match for specific files can be rather smarter by finding
387
# the IDs of those files up front and then considering only that.
389
for file_id in old_tree:
390
if file_id in new_tree:
391
kind = old_inv.get_file_kind(file_id)
392
assert kind == new_inv.get_file_kind(file_id)
394
assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
395
'invalid file kind %r' % kind
397
if kind == 'root_directory':
400
old_path = old_inv.id2path(file_id)
401
new_path = new_inv.id2path(file_id)
403
old_ie = old_inv[file_id]
404
new_ie = new_inv[file_id]
407
if (not is_inside_any(specific_files, old_path)
408
and not is_inside_any(specific_files, new_path)):
412
old_sha1 = old_tree.get_file_sha1(file_id)
413
new_sha1 = new_tree.get_file_sha1(file_id)
414
text_modified = (old_sha1 != new_sha1)
416
## mutter("no text to check for %r %r" % (file_id, kind))
417
text_modified = False
419
# TODO: Can possibly avoid calculating path strings if the
420
# two files are unchanged and their names and parents are
421
# the same and the parents are unchanged all the way up.
422
# May not be worthwhile.
424
if (old_ie.name != new_ie.name
425
or old_ie.parent_id != new_ie.parent_id):
426
delta.renamed.append((old_path, new_path, file_id, kind,
429
delta.modified.append((new_path, file_id, kind))
431
delta.unchanged.append((new_path, file_id, kind))
433
kind = old_inv.get_file_kind(file_id)
434
old_path = old_inv.id2path(file_id)
436
if not is_inside_any(specific_files, old_path):
438
delta.removed.append((old_path, file_id, kind))
440
mutter('start looking for new files')
441
for file_id in new_inv:
442
if file_id in old_inv:
444
new_path = new_inv.id2path(file_id)
446
if not is_inside_any(specific_files, new_path):
448
kind = new_inv.get_file_kind(file_id)
449
delta.added.append((new_path, file_id, kind))
454
delta.modified.sort()
455
delta.unchanged.sort()