1
# -*- coding: UTF-8 -*-
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.trace import mutter
25
# compatability - plugins import compare_trees from diff!!!
26
# deprecated as of 0.10
27
from bzrlib.delta import compare_trees
18
28
from bzrlib.errors import BzrError
19
from bzrlib.delta import compare_trees
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
36
from bzrlib.trace import mutter, warning
21
39
# TODO: Rather than building a changeset object, we should probably
22
40
# invoke callbacks on an object. That object can either accumulate a
23
41
# list, write them out directly, etc etc.
25
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file):
43
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
44
allow_binary=False, sequence_matcher=None,
45
path_encoding='utf8'):
28
46
# FIXME: difflib is wrong if there is no trailing newline.
29
47
# The syntax used by patch seems to be "\ No newline at
30
48
# end of file" following the last diff line from that
92
"""Set the env var LANG=C"""
93
os.environ['LANG'] = 'C'
96
def _spawn_external_diff(diffcmd, capture_errors=True):
97
"""Spawn the externall diff process, and return the child handle.
99
:param diffcmd: The command list to spawn
100
:param capture_errors: Capture stderr as well as setting LANG=C.
101
This lets us read and understand the output of diff, and respond
103
:return: A Popen object.
106
preexec_fn = _set_lang_C
107
stderr = subprocess.PIPE
113
pipe = subprocess.Popen(diffcmd,
114
stdin=subprocess.PIPE,
115
stdout=subprocess.PIPE,
117
preexec_fn=preexec_fn)
119
if e.errno == errno.ENOENT:
120
raise errors.NoDiff(str(e))
66
126
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
68
128
"""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",
75
129
# make sure our own output is properly ordered before the diff
78
from tempfile import NamedTemporaryFile
81
oldtmpf = NamedTemporaryFile()
82
newtmpf = NamedTemporaryFile()
132
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
133
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
134
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
135
newtmpf = os.fdopen(newtmp_fd, 'wb')
85
138
# TODO: perhaps a special case for comparing to or from the empty
128
183
diffcmd.extend(diff_opts)
130
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
185
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
186
out,err = pipe.communicate()
132
if rc != 0 and rc != 1:
189
# internal_diff() adds a trailing newline, add one here for consistency
192
# 'diff' gives retcode == 2 for all sorts of errors
193
# one of those is 'Binary files differ'.
194
# Bad options could also be the problem.
195
# 'Binary files' is not a real error, so we suppress that error.
198
# Since we got here, we want to make sure to give an i18n error
199
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
200
out, err = pipe.communicate()
202
# Write out the new i18n diff response
203
to_file.write(out+'\n')
204
if pipe.returncode != 2:
205
raise BzrError('external diff failed with exit code 2'
206
' when run with LANG=C, but not when run'
207
' natively: %r' % (diffcmd,))
209
first_line = lang_c_out.split('\n', 1)[0]
210
# Starting with diffutils 2.8.4 the word "binary" was dropped.
211
m = re.match('^(binary )?files.*differ$', first_line, re.I)
213
raise BzrError('external diff failed with exit code 2;'
214
' command: %r' % (diffcmd,))
216
# Binary files differ, just return
219
# If we got to here, we haven't written out the output of diff
133
223
# returns 1 if files differ; that's OK
135
225
msg = 'signal %d' % (-rc)
137
227
msg = 'exit code %d' % rc
139
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
229
raise BzrError('external diff failed with %s; command: %r'
141
234
oldtmpf.close() # and delete
236
# Clean up. Warn in case the files couldn't be deleted
237
# (in case windows still holds the file open, but not
238
# if the files have already been deleted)
240
os.remove(old_abspath)
242
if e.errno not in (errno.ENOENT,):
243
warning('Failed to delete temporary file: %s %s',
246
os.remove(new_abspath)
248
if e.errno not in (errno.ENOENT,):
249
warning('Failed to delete temporary file: %s %s',
253
@deprecated_function(zero_eight)
144
254
def show_diff(b, from_spec, specific_files, external_diff_options=None,
145
255
revision2=None, output=None, b2=None):
146
256
"""Shortcut for showing the diff to the working tree.
258
Please use show_diff_trees instead.
155
267
supplies any two trees.
157
269
if output is None:
159
270
output = sys.stdout
161
272
if from_spec is None:
273
old_tree = b.bzrdir.open_workingtree()
163
old_tree = b.basis_tree()
165
old_tree = b.working_tree()
275
old_tree = old_tree = old_tree.basis_tree()
167
old_tree = b.revision_tree(from_spec.in_history(b).rev_id)
277
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
169
279
if revision2 is None:
171
new_tree = b.working_tree()
281
new_tree = b.bzrdir.open_workingtree()
173
new_tree = b2.working_tree()
283
new_tree = b2.bzrdir.open_workingtree()
175
new_tree = b.revision_tree(revision2.in_history(b).rev_id)
285
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
177
287
return show_diff_trees(old_tree, new_tree, output, specific_files,
178
288
external_diff_options)
291
def diff_cmd_helper(tree, specific_files, external_diff_options,
292
old_revision_spec=None, new_revision_spec=None,
293
old_label='a/', new_label='b/'):
294
"""Helper for cmd_diff.
300
The specific files to compare, or None
302
external_diff_options
303
If non-None, run an external diff, and pass it these options
306
If None, use basis tree as old revision, otherwise use the tree for
307
the specified revision.
310
If None, use working tree as new revision, otherwise use the tree for
311
the specified revision.
313
The more general form is show_diff_trees(), where the caller
314
supplies any two trees.
318
revision = spec.in_store(tree.branch)
320
revision = spec.in_store(None)
321
revision_id = revision.rev_id
322
branch = revision.branch
323
return branch.repository.revision_tree(revision_id)
324
if old_revision_spec is None:
325
old_tree = tree.basis_tree()
327
old_tree = spec_tree(old_revision_spec)
329
if new_revision_spec is None:
332
new_tree = spec_tree(new_revision_spec)
333
if new_tree is not tree:
334
extra_trees = (tree,)
338
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
339
external_diff_options,
340
old_label=old_label, new_label=new_label,
341
extra_trees=extra_trees)
182
344
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
183
external_diff_options=None):
345
external_diff_options=None,
346
old_label='a/', new_label='b/',
184
348
"""Show in text form the changes from one tree to another.
189
353
external_diff_options
190
354
If set, use an external GNU diff and pass these options.
357
If set, more Trees to use for looking up file ids
193
# TODO: Options to control putting on a prefix or suffix, perhaps as a format string
197
DEVNULL = '/dev/null'
198
# Windows users, don't panic about this filename -- it is a
199
# special signal to GNU patch that the file should be created or
200
# deleted respectively.
363
return _show_diff_trees(old_tree, new_tree, to_file,
364
specific_files, external_diff_options,
365
old_label=old_label, new_label=new_label,
366
extra_trees=extra_trees)
373
def _show_diff_trees(old_tree, new_tree, to_file,
374
specific_files, external_diff_options,
375
old_label='a/', new_label='b/', extra_trees=None):
377
# GNU Patch uses the epoch date to detect files that are being added
378
# or removed in a diff.
379
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
202
381
# TODO: Generation of pseudo-diffs for added/deleted files could
203
382
# be usefully made into a much faster special case.
211
390
diff_file = internal_diff
214
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
215
specific_files=specific_files)
392
delta = new_tree.changes_from(old_tree,
393
specific_files=specific_files,
394
extra_trees=extra_trees, require_versioned=True)
218
397
for path, file_id, kind in delta.removed:
220
print >>to_file, '=== removed %s %r' % (kind, path)
221
old_tree.inventory[file_id].diff(diff_file, old_label + path, old_tree,
222
DEVNULL, None, None, to_file)
399
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
400
old_name = '%s%s\t%s' % (old_label, path,
401
_patch_header_date(old_tree, file_id, path))
402
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
403
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
404
new_name, None, None, to_file)
223
405
for path, file_id, kind in delta.added:
225
print >>to_file, '=== added %s %r' % (kind, path)
226
new_tree.inventory[file_id].diff(diff_file, new_label + path, new_tree,
227
DEVNULL, None, None, to_file,
407
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
408
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
409
new_name = '%s%s\t%s' % (new_label, path,
410
_patch_header_date(new_tree, file_id, path))
411
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
412
old_name, None, None, to_file,
229
414
for (old_path, new_path, file_id, kind,
230
415
text_modified, meta_modified) in delta.renamed:
232
417
prop_str = get_prop_change(meta_modified)
233
418
print >>to_file, '=== renamed %s %r => %r%s' % (
234
kind, old_path, new_path, prop_str)
235
_maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
236
new_label, new_path, new_tree,
419
kind, old_path.encode('utf8'),
420
new_path.encode('utf8'), prop_str)
421
old_name = '%s%s\t%s' % (old_label, old_path,
422
_patch_header_date(old_tree, file_id,
424
new_name = '%s%s\t%s' % (new_label, new_path,
425
_patch_header_date(new_tree, file_id,
427
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
237
429
text_modified, kind, to_file, diff_file)
238
430
for path, file_id, kind, text_modified, meta_modified in delta.modified:
240
432
prop_str = get_prop_change(meta_modified)
241
print >>to_file, '=== modified %s %r%s' % (kind, path, prop_str)
433
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
434
old_name = '%s%s\t%s' % (old_label, path,
435
_patch_header_date(old_tree, file_id, path))
436
new_name = '%s%s\t%s' % (new_label, path,
437
_patch_header_date(new_tree, file_id, path))
242
438
if text_modified:
243
_maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
244
new_label, path, new_tree,
439
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
245
441
True, kind, to_file, diff_file)
246
443
return has_changes
446
def _patch_header_date(tree, file_id, path):
447
"""Returns a timestamp suitable for use in a patch header."""
448
tm = time.gmtime(tree.get_file_mtime(file_id, path))
449
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
452
def _raise_if_nonexistent(paths, old_tree, new_tree):
453
"""Complain if paths are not in either inventory or tree.
455
It's OK with the files exist in either tree's inventory, or
456
if they exist in the tree but are not versioned.
458
This can be used by operations such as bzr status that can accept
459
unknown or ignored files.
461
mutter("check paths: %r", paths)
464
s = old_tree.filter_unversioned_files(paths)
465
s = new_tree.filter_unversioned_files(s)
466
s = [path for path in s if not new_tree.has_filename(path)]
468
raise errors.PathsDoNotExist(sorted(s))
249
471
def get_prop_change(meta_modified):
250
472
if meta_modified: