60
37
# both sequences are empty.
61
38
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)
43
if oldlines and (oldlines[-1][-1] != '\n'):
46
if newlines and (newlines[-1][-1] != '\n'):
50
ud = difflib.unified_diff(oldlines, newlines,
51
fromfile=old_label, tofile=new_label)
76
53
# work-around for difflib being too smart for its own good
77
54
# if /dev/null is "1,0", patch won't recognize it as /dev/null
79
57
ud[2] = ud[2].replace('-1,0', '-0,0')
81
60
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")
62
to_file.writelines(ud)
64
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,
70
def external_diff(old_label, oldlines, new_label, newlines, to_file,
138
72
"""Display a diff by calling out to the external diff program."""
75
if to_file != sys.stdout:
76
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
139
79
# 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')
82
from tempfile import NamedTemporaryFile
85
oldtmpf = NamedTemporaryFile()
86
newtmpf = NamedTemporaryFile()
148
89
# TODO: perhaps a special case for comparing to or from the empty
193
132
diffcmd.extend(diff_opts)
195
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
196
out,err = pipe.communicate()
134
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
136
if rc != 0 and rc != 1:
233
137
# returns 1 if files differ; that's OK
235
139
msg = 'signal %d' % (-rc)
237
141
msg = 'exit code %d' % rc
239
raise BzrError('external diff failed with %s; command: %r'
143
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
244
145
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):
150
def show_diff(b, revision, specific_files, external_diff_options=None):
266
151
"""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)
157
None for each, or otherwise the old revision to compare against.
159
The more general form is show_diff_trees(), where the caller
160
supplies any two trees.
165
old_tree = b.basis_tree()
167
old_tree = b.revision_tree(b.lookup_revision(revision))
169
new_tree = b.working_tree()
171
show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
172
external_diff_options)
354
176
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/',
177
external_diff_options=None):
358
178
"""Show in text form the changes from one tree to another.
400
205
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)
208
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
209
specific_files=specific_files)
407
211
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)
212
print >>to_file, '*** removed %s %r' % (kind, path)
214
diff_file(old_label + path,
215
old_tree.get_file(file_id).readlines(),
415
220
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))
221
print >>to_file, '*** added %s %r' % (kind, path)
226
new_tree.get_file(file_id).readlines(),
229
for old_path, new_path, file_id, kind, text_modified in delta.renamed:
230
print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
448
231
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.
232
diff_file(old_label + old_path,
233
old_tree.get_file(file_id).readlines(),
234
new_label + new_path,
235
new_tree.get_file(file_id).readlines(),
238
for path, file_id, kind in delta.modified:
239
print >>to_file, '*** modified %s %r' % (kind, path)
241
diff_file(old_label + path,
242
old_tree.get_file(file_id).readlines(),
244
new_tree.get_file(file_id).readlines(),
249
class TreeDelta(object):
250
"""Describes changes from one tree to another.
259
(oldpath, newpath, id, kind, text_modified)
265
Each id is listed only once.
267
Files that are both modified and renamed are listed only in
268
renamed, with the text_modified flag true.
270
The lists are normally sorted when the delta is created.
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)
301
def touches_file_id(self, file_id):
302
"""Return True if file_id is modified by this delta."""
303
for l in self.added, self.removed, self.modified:
307
for v in self.renamed:
313
def show(self, to_file, show_ids=False, show_unchanged=False):
314
def show_list(files):
315
for path, fid, kind in files:
316
if kind == 'directory':
318
elif kind == 'symlink':
322
print >>to_file, ' %-30s %s' % (path, fid)
324
print >>to_file, ' ', path
327
print >>to_file, 'removed:'
328
show_list(self.removed)
331
print >>to_file, 'added:'
332
show_list(self.added)
335
print >>to_file, 'renamed:'
336
for oldpath, newpath, fid, kind, text_modified in self.renamed:
338
print >>to_file, ' %s => %s %s' % (oldpath, newpath, fid)
340
print >>to_file, ' %s => %s' % (oldpath, newpath)
343
print >>to_file, 'modified:'
344
show_list(self.modified)
346
if show_unchanged and self.unchanged:
347
print >>to_file, 'unchanged:'
348
show_list(self.unchanged)
352
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
353
"""Describe changes from one tree to another.
355
Returns a TreeDelta with details of added, modified, renamed, and
358
The root entry is specifically exempt.
360
This only considers versioned files.
363
If true, also list files unchanged from one version to
367
If true, only check for changes to specified names or
371
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,
373
old_inv = old_tree.inventory
374
new_inv = new_tree.inventory
376
mutter('start compare_trees')
378
# TODO: match for specific files can be rather smarter by finding
379
# the IDs of those files up front and then considering only that.
381
for file_id in old_tree:
382
if file_id in new_tree:
383
kind = old_inv.get_file_kind(file_id)
384
assert kind == new_inv.get_file_kind(file_id)
386
assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
387
'invalid file kind %r' % kind
389
if kind == 'root_directory':
392
old_path = old_inv.id2path(file_id)
393
new_path = new_inv.id2path(file_id)
396
if (not is_inside_any(specific_files, old_path)
397
and not is_inside_any(specific_files, new_path)):
401
old_sha1 = old_tree.get_file_sha1(file_id)
402
new_sha1 = new_tree.get_file_sha1(file_id)
403
text_modified = (old_sha1 != new_sha1)
405
## mutter("no text to check for %r %r" % (file_id, kind))
406
text_modified = False
408
# TODO: Can possibly avoid calculating path strings if the
409
# two files are unchanged and their names and parents are
410
# the same and the parents are unchanged all the way up.
411
# May not be worthwhile.
413
if old_path != new_path:
414
delta.renamed.append((old_path, new_path, file_id, kind,
417
delta.modified.append((new_path, file_id, kind))
419
delta.unchanged.append((new_path, file_id, kind))
421
kind = old_inv.get_file_kind(file_id)
422
old_path = old_inv.id2path(file_id)
424
if not is_inside_any(specific_files, old_path):
426
delta.removed.append((old_path, file_id, kind))
428
mutter('start looking for new files')
429
for file_id in new_inv:
430
if file_id in old_inv:
432
new_path = new_inv.id2path(file_id)
434
if not is_inside_any(specific_files, new_path):
436
kind = new_inv.get_file_kind(file_id)
437
delta.added.append((new_path, file_id, kind))
442
delta.modified.sort()
443
delta.unchanged.sort()