14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
25
37
# compatability - plugins import compare_trees from diff!!!
26
38
# deprecated as of 0.10
27
39
from bzrlib.delta import compare_trees
28
from bzrlib.errors import BzrError
29
import bzrlib.errors as errors
31
from bzrlib.patiencediff import unified_diff
32
import bzrlib.patiencediff
33
from bzrlib.symbol_versioning import (deprecated_function,
35
from bzrlib.textfile import check_text_lines
40
from bzrlib.symbol_versioning import (
36
44
from bzrlib.trace import mutter, warning
62
70
if allow_binary is False:
63
check_text_lines(oldlines)
64
check_text_lines(newlines)
71
textfile.check_text_lines(oldlines)
72
textfile.check_text_lines(newlines)
66
74
if sequence_matcher is None:
67
sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
68
ud = unified_diff(oldlines, newlines,
75
sequence_matcher = patiencediff.PatienceSequenceMatcher
76
ud = patiencediff.unified_diff(oldlines, newlines,
69
77
fromfile=old_filename.encode(path_encoding),
70
78
tofile=new_filename.encode(path_encoding),
71
79
sequencematcher=sequence_matcher)
99
def _spawn_external_diff(diffcmd, capture_errors=True):
100
"""Spawn the externall diff process, and return the child handle.
102
:param diffcmd: The command list to spawn
103
:param capture_errors: Capture stderr as well as setting LANG=C
104
and LC_ALL=C. This lets us read and understand the output of diff,
105
and respond to any errors.
106
:return: A Popen object.
109
# construct minimal environment
111
path = os.environ.get('PATH')
114
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
117
stderr = subprocess.PIPE
123
pipe = subprocess.Popen(diffcmd,
124
stdin=subprocess.PIPE,
125
stdout=subprocess.PIPE,
129
if e.errno == errno.ENOENT:
130
raise errors.NoDiff(str(e))
91
136
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
93
138
"""Display a diff by calling out to the external diff program."""
148
193
diffcmd.extend(diff_opts)
151
pipe = subprocess.Popen(diffcmd,
152
stdin=subprocess.PIPE,
153
stdout=subprocess.PIPE)
155
if e.errno == errno.ENOENT:
156
raise errors.NoDiff(str(e))
160
first_line = pipe.stdout.readline()
161
to_file.write(first_line)
162
bzrlib.osutils.pumpfile(pipe.stdout, to_file)
195
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
196
out,err = pipe.communicate()
199
# internal_diff() adds a trailing newline, add one here for consistency
166
202
# 'diff' gives retcode == 2 for all sorts of errors
167
203
# one of those is 'Binary files differ'.
168
204
# Bad options could also be the problem.
169
# 'Binary files' is not a real error, so we suppress that error
170
m = re.match('^binary files.*differ$', first_line, re.I)
172
raise BzrError('external diff failed with exit code 2;'
173
' command: %r' % (diffcmd,))
174
elif rc not in (0, 1):
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 errors.BzrError(
216
'external diff failed with exit code 2'
217
' when run with LANG=C and LC_ALL=C,'
218
' but not when run natively: %r' % (diffcmd,))
220
first_line = lang_c_out.split('\n', 1)[0]
221
# Starting with diffutils 2.8.4 the word "binary" was dropped.
222
m = re.match('^(binary )?files.*differ$', first_line, re.I)
224
raise errors.BzrError('external diff failed with exit code 2;'
225
' command: %r' % (diffcmd,))
227
# Binary files differ, just return
230
# If we got to here, we haven't written out the output of diff
175
234
# returns 1 if files differ; that's OK
177
236
msg = 'signal %d' % (-rc)
179
238
msg = 'exit code %d' % rc
181
raise BzrError('external diff failed with %s; command: %r'
240
raise errors.BzrError('external diff failed with %s; command: %r'
184
# internal_diff() adds a trailing newline, add one here for consistency
188
245
oldtmpf.close() # and delete
245
302
def diff_cmd_helper(tree, specific_files, external_diff_options,
246
303
old_revision_spec=None, new_revision_spec=None,
247
305
old_label='a/', new_label='b/'):
248
306
"""Helper for cmd_diff.
311
:param specific_files:
254
312
The specific files to compare, or None
256
external_diff_options
314
:param external_diff_options:
257
315
If non-None, run an external diff, and pass it these options
317
:param old_revision_spec:
260
318
If None, use basis tree as old revision, otherwise use the tree for
261
319
the specified revision.
321
:param new_revision_spec:
264
322
If None, use working tree as new revision, otherwise use the tree for
265
323
the specified revision.
325
:param revision_specs:
326
Zero, one or two RevisionSpecs from the command line, saying what revisions
327
to compare. This can be passed as an alternative to the old_revision_spec
328
and new_revision_spec parameters.
267
330
The more general form is show_diff_trees(), where the caller
268
331
supplies any two trees.
334
# TODO: perhaps remove the old parameters old_revision_spec and
335
# new_revision_spec, since this is only really for use from cmd_diff and
336
# it now always passes through a sequence of revision_specs -- mbp
270
339
def spec_tree(spec):
272
341
revision = spec.in_store(tree.branch)
275
344
revision_id = revision.rev_id
276
345
branch = revision.branch
277
346
return branch.repository.revision_tree(revision_id)
348
if revision_specs is not None:
349
assert (old_revision_spec is None
350
and new_revision_spec is None)
351
if len(revision_specs) > 0:
352
old_revision_spec = revision_specs[0]
353
if len(revision_specs) > 1:
354
new_revision_spec = revision_specs[1]
278
356
if old_revision_spec is None:
279
357
old_tree = tree.basis_tree()
281
359
old_tree = spec_tree(old_revision_spec)
283
if new_revision_spec is None:
361
if (new_revision_spec is None
362
or new_revision_spec.spec is None):
286
365
new_tree = spec_tree(new_revision_spec)
287
367
if new_tree is not tree:
288
368
extra_trees = (tree,)
351
437
for path, file_id, kind in delta.removed:
353
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
439
print >>to_file, "=== removed %s '%s'" % (kind, path.encode('utf8'))
354
440
old_name = '%s%s\t%s' % (old_label, path,
355
441
_patch_header_date(old_tree, file_id, path))
356
442
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
358
444
new_name, None, None, to_file)
359
445
for path, file_id, kind in delta.added:
361
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
447
print >>to_file, "=== added %s '%s'" % (kind, path.encode('utf8'))
362
448
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
363
449
new_name = '%s%s\t%s' % (new_label, path,
364
450
_patch_header_date(new_tree, file_id, path))
369
455
text_modified, meta_modified) in delta.renamed:
371
457
prop_str = get_prop_change(meta_modified)
372
print >>to_file, '=== renamed %s %r => %r%s' % (
458
print >>to_file, "=== renamed %s '%s' => %r%s" % (
373
459
kind, old_path.encode('utf8'),
374
460
new_path.encode('utf8'), prop_str)
375
461
old_name = '%s%s\t%s' % (old_label, old_path,
384
470
for path, file_id, kind, text_modified, meta_modified in delta.modified:
386
472
prop_str = get_prop_change(meta_modified)
387
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
388
old_name = '%s%s\t%s' % (old_label, path,
389
_patch_header_date(old_tree, file_id, path))
473
print >>to_file, "=== modified %s '%s'%s" % (kind, path.encode('utf8'),
475
# The file may be in a different location in the old tree (because
476
# the containing dir was renamed, but the file itself was not)
477
old_path = old_tree.id2path(file_id)
478
old_name = '%s%s\t%s' % (old_label, old_path,
479
_patch_header_date(old_tree, file_id, old_path))
390
480
new_name = '%s%s\t%s' % (new_label, path,
391
481
_patch_header_date(new_tree, file_id, path))
392
482
if text_modified:
400
490
def _patch_header_date(tree, file_id, path):
401
491
"""Returns a timestamp suitable for use in a patch header."""
402
tm = time.gmtime(tree.get_file_mtime(file_id, path))
403
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
492
mtime = tree.get_file_mtime(file_id, path)
493
assert mtime is not None, \
494
"got an mtime of None for file-id %s, path %s in tree %s" % (
496
return timestamp.format_patch_date(mtime)
406
499
def _raise_if_nonexistent(paths, old_tree, new_tree):