1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
2
# -*- coding: UTF-8 -*-
3
4
# This program is free software; you can redistribute it and/or modify
4
5
# it under the terms of the GNU General Public License as published by
5
6
# the Free Software Foundation; either version 2 of the License, or
6
7
# (at your option) any later version.
8
9
# This program is distributed in the hope that it will be useful,
9
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
12
# GNU General Public License for more details.
13
14
# You should have received a copy of the GNU General Public License
14
15
# along with this program; if not, write to the Free Software
15
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
from bzrlib.lazy_import import lazy_import
22
lazy_import(globals(), """
37
# compatability - plugins import compare_trees from diff!!!
38
# deprecated as of 0.10
18
from bzrlib.trace import mutter
19
from bzrlib.errors import BzrError
39
20
from bzrlib.delta import compare_trees
40
from bzrlib.symbol_versioning import (
44
from bzrlib.trace import mutter, warning
47
22
# TODO: Rather than building a changeset object, we should probably
48
23
# invoke callbacks on an object. That object can either accumulate a
49
24
# list, write them out directly, etc etc.
51
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
52
allow_binary=False, sequence_matcher=None,
53
path_encoding='utf8'):
26
def internal_diff(old_label, oldlines, new_label, newlines, to_file):
54
29
# FIXME: difflib is wrong if there is no trailing newline.
55
30
# The syntax used by patch seems to be "\ No newline at
56
31
# end of file" following the last diff line from that
66
41
# both sequences are empty.
67
42
if not oldlines and not newlines:
70
if allow_binary is False:
71
textfile.check_text_lines(oldlines)
72
textfile.check_text_lines(newlines)
74
if sequence_matcher is None:
75
sequence_matcher = patiencediff.PatienceSequenceMatcher
76
ud = patiencediff.unified_diff(oldlines, newlines,
77
fromfile=old_filename.encode(path_encoding),
78
tofile=new_filename.encode(path_encoding),
79
sequencematcher=sequence_matcher)
45
ud = difflib.unified_diff(oldlines, newlines,
46
fromfile=old_label, tofile=new_label)
82
48
# work-around for difflib being too smart for its own good
83
49
# if /dev/null is "1,0", patch won't recognize it as /dev/null
85
52
ud[2] = ud[2].replace('-1,0', '-0,0')
87
55
ud[2] = ud[2].replace('+1,0', '+0,0')
88
# work around for difflib emitting random spaces after the label
89
ud[0] = ud[0][:-2] + '\n'
90
ud[1] = ud[1][:-2] + '\n'
93
58
to_file.write(line)
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))
136
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
66
def external_diff(old_label, oldlines, new_label, newlines, to_file,
138
68
"""Display a diff by calling out to the external diff program."""
71
if to_file != sys.stdout:
72
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
139
75
# 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')
78
from tempfile import NamedTemporaryFile
81
oldtmpf = NamedTemporaryFile()
82
newtmpf = NamedTemporaryFile()
148
85
# TODO: perhaps a special case for comparing to or from the empty
193
128
diffcmd.extend(diff_opts)
195
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
196
out,err = pipe.communicate()
130
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 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
132
if rc != 0 and rc != 1:
234
133
# returns 1 if files differ; that's OK
236
135
msg = 'signal %d' % (-rc)
238
137
msg = 'exit code %d' % rc
240
raise errors.BzrError('external diff failed with %s; command: %r'
139
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
245
141
oldtmpf.close() # and delete
247
# Clean up. Warn in case the files couldn't be deleted
248
# (in case windows still holds the file open, but not
249
# if the files have already been deleted)
251
os.remove(old_abspath)
253
if e.errno not in (errno.ENOENT,):
254
warning('Failed to delete temporary file: %s %s',
257
os.remove(new_abspath)
259
if e.errno not in (errno.ENOENT,):
260
warning('Failed to delete temporary file: %s %s',
264
@deprecated_function(zero_eight)
265
def show_diff(b, from_spec, specific_files, external_diff_options=None,
266
revision2=None, output=None, b2=None):
146
def show_diff(b, revision, specific_files, external_diff_options=None,
267
148
"""Shortcut for showing the diff to the working tree.
269
Please use show_diff_trees instead.
275
None for 'basis tree', or otherwise the old revision to compare against.
277
The more general form is show_diff_trees(), where the caller
278
supplies any two trees.
283
if from_spec is None:
284
old_tree = b.bzrdir.open_workingtree()
286
old_tree = old_tree = old_tree.basis_tree()
288
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
290
if revision2 is None:
292
new_tree = b.bzrdir.open_workingtree()
294
new_tree = b2.bzrdir.open_workingtree()
296
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
298
return show_diff_trees(old_tree, new_tree, output, specific_files,
299
external_diff_options)
302
def diff_cmd_helper(tree, specific_files, external_diff_options,
303
old_revision_spec=None, new_revision_spec=None,
305
old_label='a/', new_label='b/'):
306
"""Helper for cmd_diff.
311
:param specific_files:
312
The specific files to compare, or None
314
:param external_diff_options:
315
If non-None, run an external diff, and pass it these options
317
:param old_revision_spec:
318
If None, use basis tree as old revision, otherwise use the tree for
319
the specified revision.
321
:param new_revision_spec:
322
If None, use working tree as new revision, otherwise use the tree for
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.
330
The more general form is show_diff_trees(), where the caller
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
341
revision = spec.in_store(tree.branch)
343
revision = spec.in_store(None)
344
revision_id = revision.rev_id
345
branch = revision.branch
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]
356
if old_revision_spec is None:
357
old_tree = tree.basis_tree()
359
old_tree = spec_tree(old_revision_spec)
361
if (new_revision_spec is None
362
or new_revision_spec.spec is None):
365
new_tree = spec_tree(new_revision_spec)
367
if new_tree is not tree:
368
extra_trees = (tree,)
372
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
373
external_diff_options,
374
old_label=old_label, new_label=new_label,
375
extra_trees=extra_trees)
154
None for each, or otherwise the old revision to compare against.
156
The more general form is show_diff_trees(), where the caller
157
supplies any two trees.
162
old_tree = b.basis_tree()
164
old_tree = b.revision_tree(b.lookup_revision(revision))
166
if revision2 == None:
167
new_tree = b.working_tree()
169
new_tree = b.revision_tree(b.lookup_revision(revision2))
171
show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
172
external_diff_options)
378
176
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
379
external_diff_options=None,
380
old_label='a/', new_label='b/',
177
external_diff_options=None):
382
178
"""Show in text form the changes from one tree to another.
387
183
external_diff_options
388
184
If set, use an external GNU diff and pass these options.
391
If set, more Trees to use for looking up file ids
395
if extra_trees is not None:
396
for tree in extra_trees:
400
return _show_diff_trees(old_tree, new_tree, to_file,
401
specific_files, external_diff_options,
402
old_label=old_label, new_label=new_label,
403
extra_trees=extra_trees)
406
if extra_trees is not None:
407
for tree in extra_trees:
413
def _show_diff_trees(old_tree, new_tree, to_file,
414
specific_files, external_diff_options,
415
old_label='a/', new_label='b/', extra_trees=None):
417
# GNU Patch uses the epoch date to detect files that are being added
418
# or removed in a diff.
419
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
187
# TODO: Options to control putting on a prefix or suffix, perhaps as a format string
191
DEVNULL = '/dev/null'
192
# Windows users, don't panic about this filename -- it is a
193
# special signal to GNU patch that the file should be created or
194
# deleted respectively.
421
196
# TODO: Generation of pseudo-diffs for added/deleted files could
422
197
# be usefully made into a much faster special case.
430
205
diff_file = internal_diff
432
delta = new_tree.changes_from(old_tree,
433
specific_files=specific_files,
434
extra_trees=extra_trees, require_versioned=True)
208
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
209
specific_files=specific_files)
437
211
for path, file_id, kind in delta.removed:
439
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
440
old_name = '%s%s\t%s' % (old_label, path,
441
_patch_header_date(old_tree, file_id, path))
442
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
443
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
444
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(),
445
220
for path, file_id, kind in delta.added:
447
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
448
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
449
new_name = '%s%s\t%s' % (new_label, path,
450
_patch_header_date(new_tree, file_id, path))
451
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
452
old_name, None, None, to_file,
454
for (old_path, new_path, file_id, kind,
455
text_modified, meta_modified) in delta.renamed:
457
prop_str = get_prop_change(meta_modified)
458
print >>to_file, '=== renamed %s %r => %r%s' % (
459
kind, old_path.encode('utf8'),
460
new_path.encode('utf8'), prop_str)
461
old_name = '%s%s\t%s' % (old_label, old_path,
462
_patch_header_date(old_tree, file_id,
464
new_name = '%s%s\t%s' % (new_label, new_path,
465
_patch_header_date(new_tree, file_id,
467
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
469
text_modified, kind, to_file, diff_file)
470
for path, file_id, kind, text_modified, meta_modified in delta.modified:
472
prop_str = get_prop_change(meta_modified)
473
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
474
# The file may be in a different location in the old tree (because
475
# the containing dir was renamed, but the file itself was not)
476
old_path = old_tree.id2path(file_id)
477
old_name = '%s%s\t%s' % (old_label, old_path,
478
_patch_header_date(old_tree, file_id, old_path))
479
new_name = '%s%s\t%s' % (new_label, path,
480
_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)
481
231
if text_modified:
482
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
484
True, kind, to_file, diff_file)
489
def _patch_header_date(tree, file_id, path):
490
"""Returns a timestamp suitable for use in a patch header."""
491
mtime = tree.get_file_mtime(file_id, path)
492
assert mtime is not None, \
493
"got an mtime of None for file-id %s, path %s in tree %s" % (
495
return timestamp.format_patch_date(mtime)
498
def _raise_if_nonexistent(paths, old_tree, new_tree):
499
"""Complain if paths are not in either inventory or tree.
501
It's OK with the files exist in either tree's inventory, or
502
if they exist in the tree but are not versioned.
504
This can be used by operations such as bzr status that can accept
505
unknown or ignored files.
507
mutter("check paths: %r", paths)
510
s = old_tree.filter_unversioned_files(paths)
511
s = new_tree.filter_unversioned_files(s)
512
s = [path for path in s if not new_tree.has_filename(path)]
514
raise errors.PathsDoNotExist(sorted(s))
517
def get_prop_change(meta_modified):
519
return " (properties changed)"
524
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
525
new_path, new_tree, text_modified,
526
kind, to_file, diff_file):
528
new_entry = new_tree.inventory[file_id]
529
old_tree.inventory[file_id].diff(diff_file,
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(),