1
1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
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
17
from bzrlib.delta import compare_trees
18
from bzrlib.errors import BzrError
19
import bzrlib.errors as errors
20
from bzrlib.symbol_versioning import *
21
from bzrlib.trace import mutter
22
from bzrlib.lazy_import import lazy_import
23
lazy_import(globals(), """
38
from bzrlib.symbol_versioning import (
41
from bzrlib.trace import mutter, warning
23
44
# TODO: Rather than building a changeset object, we should probably
24
45
# invoke callbacks on an object. That object can either accumulate a
25
46
# list, write them out directly, etc etc.
27
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file):
49
class _PrematchedMatcher(difflib.SequenceMatcher):
50
"""Allow SequenceMatcher operations to use predetermined blocks"""
52
def __init__(self, matching_blocks):
53
difflib.SequenceMatcher(self, None, None)
54
self.matching_blocks = matching_blocks
58
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
59
allow_binary=False, sequence_matcher=None,
60
path_encoding='utf8'):
30
61
# FIXME: difflib is wrong if there is no trailing newline.
31
62
# The syntax used by patch seems to be "\ No newline at
32
63
# end of file" following the last diff line from that
106
def _spawn_external_diff(diffcmd, capture_errors=True):
107
"""Spawn the externall diff process, and return the child handle.
109
:param diffcmd: The command list to spawn
110
:param capture_errors: Capture stderr as well as setting LANG=C
111
and LC_ALL=C. This lets us read and understand the output of diff,
112
and respond to any errors.
113
:return: A Popen object.
116
# construct minimal environment
118
path = os.environ.get('PATH')
121
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
124
stderr = subprocess.PIPE
130
pipe = subprocess.Popen(diffcmd,
131
stdin=subprocess.PIPE,
132
stdout=subprocess.PIPE,
136
if e.errno == errno.ENOENT:
137
raise errors.NoDiff(str(e))
68
143
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
70
145
"""Display a diff by calling out to the external diff program."""
73
if to_file != sys.stdout:
74
raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
77
146
# make sure our own output is properly ordered before the diff
80
from tempfile import NamedTemporaryFile
83
oldtmpf = NamedTemporaryFile()
84
newtmpf = NamedTemporaryFile()
149
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
150
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
151
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
152
newtmpf = os.fdopen(newtmp_fd, 'wb')
87
155
# TODO: perhaps a special case for comparing to or from the empty
130
200
diffcmd.extend(diff_opts)
132
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
202
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
203
out,err = pipe.communicate()
134
if rc != 0 and rc != 1:
206
# internal_diff() adds a trailing newline, add one here for consistency
209
# 'diff' gives retcode == 2 for all sorts of errors
210
# one of those is 'Binary files differ'.
211
# Bad options could also be the problem.
212
# 'Binary files' is not a real error, so we suppress that error.
215
# Since we got here, we want to make sure to give an i18n error
216
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
217
out, err = pipe.communicate()
219
# Write out the new i18n diff response
220
to_file.write(out+'\n')
221
if pipe.returncode != 2:
222
raise errors.BzrError(
223
'external diff failed with exit code 2'
224
' when run with LANG=C and LC_ALL=C,'
225
' but not when run natively: %r' % (diffcmd,))
227
first_line = lang_c_out.split('\n', 1)[0]
228
# Starting with diffutils 2.8.4 the word "binary" was dropped.
229
m = re.match('^(binary )?files.*differ$', first_line, re.I)
231
raise errors.BzrError('external diff failed with exit code 2;'
232
' command: %r' % (diffcmd,))
234
# Binary files differ, just return
237
# If we got to here, we haven't written out the output of diff
135
241
# returns 1 if files differ; that's OK
137
243
msg = 'signal %d' % (-rc)
139
245
msg = 'exit code %d' % rc
141
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
247
raise errors.BzrError('external diff failed with %s; command: %r'
143
252
oldtmpf.close() # and delete
147
@deprecated_function(zero_eight)
148
def show_diff(b, from_spec, specific_files, external_diff_options=None,
149
revision2=None, output=None, b2=None):
150
"""Shortcut for showing the diff to the working tree.
152
Please use show_diff_trees instead.
158
None for 'basis tree', or otherwise the old revision to compare against.
160
The more general form is show_diff_trees(), where the caller
161
supplies any two trees.
167
if from_spec is None:
168
old_tree = b.bzrdir.open_workingtree()
170
old_tree = old_tree = old_tree.basis_tree()
172
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
174
if revision2 is None:
176
new_tree = b.bzrdir.open_workingtree()
178
new_tree = b2.bzrdir.open_workingtree()
180
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
182
return show_diff_trees(old_tree, new_tree, output, specific_files,
183
external_diff_options)
254
# Clean up. Warn in case the files couldn't be deleted
255
# (in case windows still holds the file open, but not
256
# if the files have already been deleted)
258
os.remove(old_abspath)
260
if e.errno not in (errno.ENOENT,):
261
warning('Failed to delete temporary file: %s %s',
264
os.remove(new_abspath)
266
if e.errno not in (errno.ENOENT,):
267
warning('Failed to delete temporary file: %s %s',
186
271
def diff_cmd_helper(tree, specific_files, external_diff_options,
187
old_revision_spec=None, new_revision_spec=None):
272
old_revision_spec=None, new_revision_spec=None,
274
old_label='a/', new_label='b/'):
188
275
"""Helper for cmd_diff.
280
:param specific_files:
194
281
The specific files to compare, or None
196
external_diff_options
283
:param external_diff_options:
197
284
If non-None, run an external diff, and pass it these options
286
:param old_revision_spec:
200
287
If None, use basis tree as old revision, otherwise use the tree for
201
288
the specified revision.
290
:param new_revision_spec:
204
291
If None, use working tree as new revision, otherwise use the tree for
205
292
the specified revision.
294
:param revision_specs:
295
Zero, one or two RevisionSpecs from the command line, saying what revisions
296
to compare. This can be passed as an alternative to the old_revision_spec
297
and new_revision_spec parameters.
207
299
The more general form is show_diff_trees(), where the caller
208
300
supplies any two trees.
303
# TODO: perhaps remove the old parameters old_revision_spec and
304
# new_revision_spec, since this is only really for use from cmd_diff and
305
# it now always passes through a sequence of revision_specs -- mbp
212
308
def spec_tree(spec):
213
revision_id = spec.in_store(tree.branch).rev_id
214
return tree.branch.repository.revision_tree(revision_id)
310
revision = spec.in_store(tree.branch)
312
revision = spec.in_store(None)
313
revision_id = revision.rev_id
314
branch = revision.branch
315
return branch.repository.revision_tree(revision_id)
317
if revision_specs is not None:
318
assert (old_revision_spec is None
319
and new_revision_spec is None)
320
if len(revision_specs) > 0:
321
old_revision_spec = revision_specs[0]
322
if len(revision_specs) > 1:
323
new_revision_spec = revision_specs[1]
215
325
if old_revision_spec is None:
216
326
old_tree = tree.basis_tree()
218
328
old_tree = spec_tree(old_revision_spec)
220
if new_revision_spec is None:
330
if (new_revision_spec is None
331
or new_revision_spec.spec is None):
223
334
new_tree = spec_tree(new_revision_spec)
336
if new_tree is not tree:
337
extra_trees = (tree,)
225
341
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
226
external_diff_options)
342
external_diff_options,
343
old_label=old_label, new_label=new_label,
344
extra_trees=extra_trees)
229
347
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
230
external_diff_options=None):
348
external_diff_options=None,
349
old_label='a/', new_label='b/',
231
351
"""Show in text form the changes from one tree to another.
236
356
external_diff_options
237
357
If set, use an external GNU diff and pass these options.
360
If set, more Trees to use for looking up file ids
239
362
old_tree.lock_read()
364
if extra_trees is not None:
365
for tree in extra_trees:
241
367
new_tree.lock_read()
243
369
return _show_diff_trees(old_tree, new_tree, to_file,
244
specific_files, external_diff_options)
370
specific_files, external_diff_options,
371
old_label=old_label, new_label=new_label,
372
extra_trees=extra_trees)
246
374
new_tree.unlock()
375
if extra_trees is not None:
376
for tree in extra_trees:
248
379
old_tree.unlock()
251
382
def _show_diff_trees(old_tree, new_tree, to_file,
252
specific_files, external_diff_options):
254
# TODO: Options to control putting on a prefix or suffix, perhaps
255
# as a format string?
259
DEVNULL = '/dev/null'
260
# Windows users, don't panic about this filename -- it is a
261
# special signal to GNU patch that the file should be created or
262
# deleted respectively.
383
specific_files, external_diff_options,
384
old_label='a/', new_label='b/', extra_trees=None):
386
# GNU Patch uses the epoch date to detect files that are being added
387
# or removed in a diff.
388
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
264
390
# TODO: Generation of pseudo-diffs for added/deleted files could
265
391
# be usefully made into a much faster special case.
267
_raise_if_doubly_unversioned(specific_files, old_tree, new_tree)
269
393
if external_diff_options:
270
394
assert isinstance(external_diff_options, basestring)
271
395
opts = external_diff_options.split()
275
399
diff_file = internal_diff
277
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
278
specific_files=specific_files)
401
delta = new_tree.changes_from(old_tree,
402
specific_files=specific_files,
403
extra_trees=extra_trees, require_versioned=True)
281
406
for path, file_id, kind in delta.removed:
283
print >>to_file, '=== removed %s %r' % (kind, old_label + path)
284
old_tree.inventory[file_id].diff(diff_file, old_label + path, old_tree,
285
DEVNULL, None, None, to_file)
408
print >>to_file, "=== removed %s '%s'" % (kind, path.encode('utf8'))
409
old_name = '%s%s\t%s' % (old_label, path,
410
_patch_header_date(old_tree, file_id, path))
411
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
412
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
413
new_name, None, None, to_file)
286
414
for path, file_id, kind in delta.added:
288
print >>to_file, '=== added %s %r' % (kind, new_label + path)
289
new_tree.inventory[file_id].diff(diff_file, new_label + path, new_tree,
290
DEVNULL, None, None, to_file,
416
print >>to_file, "=== added %s '%s'" % (kind, path.encode('utf8'))
417
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
418
new_name = '%s%s\t%s' % (new_label, path,
419
_patch_header_date(new_tree, file_id, path))
420
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
421
old_name, None, None, to_file,
292
423
for (old_path, new_path, file_id, kind,
293
424
text_modified, meta_modified) in delta.renamed:
295
426
prop_str = get_prop_change(meta_modified)
296
print >>to_file, '=== renamed %s %r => %r%s' % (
297
kind, old_label + old_path, new_label + new_path, prop_str)
298
_maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
299
new_label, new_path, new_tree,
427
print >>to_file, "=== renamed %s '%s' => '%s'%s" % (
428
kind, old_path.encode('utf8'),
429
new_path.encode('utf8'), prop_str)
430
old_name = '%s%s\t%s' % (old_label, old_path,
431
_patch_header_date(old_tree, file_id,
433
new_name = '%s%s\t%s' % (new_label, new_path,
434
_patch_header_date(new_tree, file_id,
436
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
300
438
text_modified, kind, to_file, diff_file)
301
439
for path, file_id, kind, text_modified, meta_modified in delta.modified:
303
441
prop_str = get_prop_change(meta_modified)
304
print >>to_file, '=== modified %s %r%s' % (kind, old_label + path,
442
print >>to_file, "=== modified %s '%s'%s" % (kind, path.encode('utf8'),
444
# The file may be in a different location in the old tree (because
445
# the containing dir was renamed, but the file itself was not)
446
old_path = old_tree.id2path(file_id)
447
old_name = '%s%s\t%s' % (old_label, old_path,
448
_patch_header_date(old_tree, file_id, old_path))
449
new_name = '%s%s\t%s' % (new_label, path,
450
_patch_header_date(new_tree, file_id, path))
306
451
if text_modified:
307
_maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
308
new_label, path, new_tree,
452
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
309
454
True, kind, to_file, diff_file)
311
456
return has_changes
314
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
315
"""Complain if paths are not versioned in either tree."""
316
if not specific_files:
459
def _patch_header_date(tree, file_id, path):
460
"""Returns a timestamp suitable for use in a patch header."""
461
mtime = tree.get_file_mtime(file_id, path)
462
assert mtime is not None, \
463
"got an mtime of None for file-id %s, path %s in tree %s" % (
465
return timestamp.format_patch_date(mtime)
468
def _raise_if_nonexistent(paths, old_tree, new_tree):
469
"""Complain if paths are not in either inventory or tree.
471
It's OK with the files exist in either tree's inventory, or
472
if they exist in the tree but are not versioned.
474
This can be used by operations such as bzr status that can accept
475
unknown or ignored files.
477
mutter("check paths: %r", paths)
318
old_unversioned = old_tree.filter_unversioned_files(specific_files)
319
new_unversioned = new_tree.filter_unversioned_files(specific_files)
320
unversioned = old_unversioned.intersection(new_unversioned)
322
raise errors.PathsNotVersionedError(sorted(unversioned))
480
s = old_tree.filter_unversioned_files(paths)
481
s = new_tree.filter_unversioned_files(s)
482
s = [path for path in s if not new_tree.has_filename(path)]
484
raise errors.PathsDoNotExist(sorted(s))
325
487
def get_prop_change(meta_modified):
326
488
if meta_modified: