2
# -*- coding: UTF-8 -*-
1
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
6
5
# the Free Software Foundation; either version 2 of the License, or
7
6
# (at your option) any later version.
9
8
# This program is distributed in the hope that it will be useful,
10
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
11
# GNU General Public License for more details.
14
13
# You should have received a copy of the GNU General Public License
15
14
# along with this program; if not, write to the Free Software
16
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
from trace import mutter
21
from errors import BzrError
24
def diff_trees(old_tree, new_tree):
25
"""Compute diff between two trees.
27
They may be in different branches and may be working or historical
30
Yields a sequence of (state, id, old_name, new_name, kind).
31
Each filename and each id is listed only once.
23
from bzrlib.lazy_import import lazy_import
24
lazy_import(globals(), """
41
from bzrlib.symbol_versioning import (
45
from bzrlib.trace import mutter, warning
48
# TODO: Rather than building a changeset object, we should probably
49
# invoke callbacks on an object. That object can either accumulate a
50
# list, write them out directly, etc etc.
53
class _PrematchedMatcher(difflib.SequenceMatcher):
54
"""Allow SequenceMatcher operations to use predetermined blocks"""
56
def __init__(self, matching_blocks):
57
difflib.SequenceMatcher(self, None, None)
58
self.matching_blocks = matching_blocks
62
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
63
allow_binary=False, sequence_matcher=None,
64
path_encoding='utf8'):
65
# FIXME: difflib is wrong if there is no trailing newline.
66
# The syntax used by patch seems to be "\ No newline at
67
# end of file" following the last diff line from that
68
# file. This is not trivial to insert into the
69
# unified_diff output and it might be better to just fix
70
# or replace that function.
72
# In the meantime we at least make sure the patch isn't
76
# Special workaround for Python2.3, where difflib fails if
77
# both sequences are empty.
78
if not oldlines and not newlines:
81
if allow_binary is False:
82
textfile.check_text_lines(oldlines)
83
textfile.check_text_lines(newlines)
85
if sequence_matcher is None:
86
sequence_matcher = patiencediff.PatienceSequenceMatcher
87
ud = patiencediff.unified_diff(oldlines, newlines,
88
fromfile=old_filename.encode(path_encoding),
89
tofile=new_filename.encode(path_encoding),
90
sequencematcher=sequence_matcher)
93
if len(ud) == 0: # Identical contents, nothing to do
95
# work-around for difflib being too smart for its own good
96
# if /dev/null is "1,0", patch won't recognize it as /dev/null
98
ud[2] = ud[2].replace('-1,0', '-0,0')
100
ud[2] = ud[2].replace('+1,0', '+0,0')
101
# work around for difflib emitting random spaces after the label
102
ud[0] = ud[0][:-2] + '\n'
103
ud[1] = ud[1][:-2] + '\n'
107
if not line.endswith('\n'):
108
to_file.write("\n\\ No newline at end of file\n")
112
def _spawn_external_diff(diffcmd, capture_errors=True):
113
"""Spawn the externall diff process, and return the child handle.
115
:param diffcmd: The command list to spawn
116
:param capture_errors: Capture stderr as well as setting LANG=C
117
and LC_ALL=C. This lets us read and understand the output of diff,
118
and respond to any errors.
119
:return: A Popen object.
34
## TODO: Compare files before diffing; only mention those that have changed
36
## TODO: Set nice names in the headers, maybe include diffstat
38
## TODO: Perhaps make this a generator rather than using
41
## TODO: Allow specifying a list of files to compare, rather than
42
## doing the whole tree? (Not urgent.)
44
## TODO: Allow diffing any two inventories, not just the
45
## current one against one. We mgiht need to specify two
46
## stores to look for the files if diffing two branches. That
47
## might imply this shouldn't be primarily a Branch method.
49
## XXX: This doesn't report on unknown files; that can be done
50
## from a separate method.
52
old_it = old_tree.list_files()
53
new_it = new_tree.list_files()
61
old_item = next(old_it)
62
new_item = next(new_it)
64
# We step through the two sorted iterators in parallel, trying to
67
while (old_item != None) or (new_item != None):
68
# OK, we still have some remaining on both, but they may be
71
old_name, old_class, old_kind, old_id = old_item
76
new_name, new_class, new_kind, new_id = new_item
80
mutter(" diff pairwise %r" % (old_item,))
81
mutter(" %r" % (new_item,))
84
# can't handle the old tree being a WorkingTree
85
assert old_class == 'V'
87
if new_item and (new_class != 'V'):
88
yield new_class, None, None, new_name, new_kind
89
new_item = next(new_it)
90
elif (not new_item) or (old_item and (old_name < new_name)):
91
mutter(" extra entry in old-tree sequence")
92
if new_tree.has_id(old_id):
93
# will be mentioned as renamed under new name
96
yield 'D', old_id, old_name, None, old_kind
97
old_item = next(old_it)
98
elif (not old_item) or (new_item and (new_name < old_name)):
99
mutter(" extra entry in new-tree sequence")
100
if old_tree.has_id(new_id):
101
yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
103
yield 'A', new_id, None, new_name, new_kind
104
new_item = next(new_it)
105
elif old_id != new_id:
106
assert old_name == new_name
107
# both trees have a file of this name, but it is not the
108
# same file. in other words, the old filename has been
109
# overwritten by either a newly-added or a renamed file.
110
# (should we return something about the overwritten file?)
111
if old_tree.has_id(new_id):
112
# renaming, overlying a deleted file
113
yield 'R', new_id, old_tree.id2path(new_id), new_name, new_kind
115
yield 'A', new_id, None, new_name, new_kind
117
new_item = next(new_it)
118
old_item = next(old_it)
120
assert old_id == new_id
121
assert old_id != None
122
assert old_name == new_name
123
assert old_kind == new_kind
125
if old_kind == 'directory':
126
yield '.', new_id, old_name, new_name, new_kind
127
elif old_tree.get_file_size(old_id) != new_tree.get_file_size(old_id):
128
mutter(" file size has changed, must be different")
129
yield 'M', new_id, old_name, new_name, new_kind
130
elif old_tree.get_file_sha1(old_id) == new_tree.get_file_sha1(old_id):
131
mutter(" SHA1 indicates they're identical")
132
## assert compare_files(old_tree.get_file(i), new_tree.get_file(i))
133
yield '.', new_id, old_name, new_name, new_kind
135
mutter(" quick compare shows different")
136
yield 'M', new_id, old_name, new_name, new_kind
138
new_item = next(new_it)
139
old_item = next(old_it)
143
def show_diff(b, revision, file_list):
144
import difflib, sys, types
147
old_tree = b.basis_tree()
122
# construct minimal environment
124
path = os.environ.get('PATH')
127
env['LANGUAGE'] = 'C' # on win32 only LANGUAGE has effect
130
stderr = subprocess.PIPE
149
old_tree = b.revision_tree(b.lookup_revision(revision))
151
new_tree = b.working_tree()
153
# TODO: Options to control putting on a prefix or suffix, perhaps as a format string
157
DEVNULL = '/dev/null'
158
# Windows users, don't panic about this filename -- it is a
159
# special signal to GNU patch that the file should be created or
160
# deleted respectively.
162
# TODO: Generation of pseudo-diffs for added/deleted files could
163
# be usefully made into a much faster special case.
165
# TODO: Better to return them in sorted order I think.
168
file_list = [b.relpath(f) for f in file_list]
170
# FIXME: If given a file list, compare only those files rather
171
# than comparing everything and then throwing stuff away.
173
for file_state, fid, old_name, new_name, kind in diff_trees(old_tree, new_tree):
175
if file_list and (new_name not in file_list):
178
# Don't show this by default; maybe do it if an option is passed
179
# idlabel = ' {%s}' % fid
182
# FIXME: Something about the diff format makes patch unhappy
183
# with newly-added files.
185
def diffit(oldlines, newlines, **kw):
187
# FIXME: difflib is wrong if there is no trailing newline.
188
# The syntax used by patch seems to be "\ No newline at
189
# end of file" following the last diff line from that
190
# file. This is not trivial to insert into the
191
# unified_diff output and it might be better to just fix
192
# or replace that function.
194
# In the meantime we at least make sure the patch isn't
198
# Special workaround for Python2.3, where difflib fails if
199
# both sequences are empty.
200
if not oldlines and not newlines:
136
pipe = subprocess.Popen(diffcmd,
137
stdin=subprocess.PIPE,
138
stdout=subprocess.PIPE,
142
if e.errno == errno.ENOENT:
143
raise errors.NoDiff(str(e))
149
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
151
"""Display a diff by calling out to the external diff program."""
152
# make sure our own output is properly ordered before the diff
155
oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
156
newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
157
oldtmpf = os.fdopen(oldtmp_fd, 'wb')
158
newtmpf = os.fdopen(newtmp_fd, 'wb')
161
# TODO: perhaps a special case for comparing to or from the empty
162
# sequence; can just use /dev/null on Unix
164
# TODO: if either of the files being compared already exists as a
165
# regular named file (e.g. in the working directory) then we can
166
# compare directly to that, rather than copying it.
168
oldtmpf.writelines(oldlines)
169
newtmpf.writelines(newlines)
177
'--label', old_filename,
179
'--label', new_filename,
184
# diff only allows one style to be specified; they don't override.
185
# note that some of these take optargs, and the optargs can be
186
# directly appended to the options.
187
# this is only an approximate parser; it doesn't properly understand
189
for s in ['-c', '-u', '-C', '-U',
194
'-y', '--side-by-side',
206
diffcmd.extend(diff_opts)
208
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
209
out,err = pipe.communicate()
212
# internal_diff() adds a trailing newline, add one here for consistency
215
# 'diff' gives retcode == 2 for all sorts of errors
216
# one of those is 'Binary files differ'.
217
# Bad options could also be the problem.
218
# 'Binary files' is not a real error, so we suppress that error.
221
# Since we got here, we want to make sure to give an i18n error
222
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
223
out, err = pipe.communicate()
225
# Write out the new i18n diff response
226
to_file.write(out+'\n')
227
if pipe.returncode != 2:
228
raise errors.BzrError(
229
'external diff failed with exit code 2'
230
' when run with LANG=C and LC_ALL=C,'
231
' but not when run natively: %r' % (diffcmd,))
233
first_line = lang_c_out.split('\n', 1)[0]
234
# Starting with diffutils 2.8.4 the word "binary" was dropped.
235
m = re.match('^(binary )?files.*differ$', first_line, re.I)
237
raise errors.BzrError('external diff failed with exit code 2;'
238
' command: %r' % (diffcmd,))
240
# Binary files differ, just return
205
if oldlines and (oldlines[-1][-1] != '\n'):
208
if newlines and (newlines[-1][-1] != '\n'):
212
ud = difflib.unified_diff(oldlines, newlines, **kw)
213
sys.stdout.writelines(ud)
215
print "\\ No newline at end of file"
216
sys.stdout.write('\n')
218
if file_state in ['.', '?', 'I']:
220
elif file_state == 'A':
221
print '*** added %s %r' % (kind, new_name)
224
new_tree.get_file(fid).readlines(),
226
tofile=new_label + new_name + idlabel)
227
elif file_state == 'D':
228
assert isinstance(old_name, types.StringTypes)
229
print '*** deleted %s %r' % (kind, old_name)
231
diffit(old_tree.get_file(fid).readlines(), [],
232
fromfile=old_label + old_name + idlabel,
234
elif file_state in ['M', 'R']:
235
if file_state == 'M':
236
assert kind == 'file'
237
assert old_name == new_name
238
print '*** modified %s %r' % (kind, new_name)
239
elif file_state == 'R':
240
print '*** renamed %s %r => %r' % (kind, old_name, new_name)
243
diffit(old_tree.get_file(fid).readlines(),
244
new_tree.get_file(fid).readlines(),
245
fromfile=old_label + old_name + idlabel,
246
tofile=new_label + new_name)
248
raise BzrError("can't represent state %s {%s}" % (file_state, fid))
253
"""Describes changes from one tree to another.
262
(oldpath, newpath, id)
266
A path may occur in more than one list if it was e.g. deleted
267
under an old id and renamed into place in a new id.
269
Files are listed in either modified or renamed, not both. In
270
other words, renamed files may also be modified.
279
def compare_inventories(old_inv, new_inv):
280
"""Return a TreeDelta object describing changes between inventories.
282
This only describes changes in the shape of the tree, not the
285
This is an alternative to diff_trees() and should probably
286
eventually replace it.
288
old_ids = old_inv.id_set()
289
new_ids = new_inv.id_set()
292
delta.removed = [(old_inv.id2path(fid), fid) for fid in (old_ids - new_ids)]
295
delta.added = [(new_inv.id2path(fid), fid) for fid in (new_ids - old_ids)]
298
for fid in old_ids & new_ids:
299
old_ie = old_inv[fid]
300
new_ie = new_inv[fid]
301
old_path = old_inv.id2path(fid)
302
new_path = new_inv.id2path(fid)
304
if old_path != new_path:
305
delta.renamed.append((old_path, new_path, fid))
306
elif old_ie.text_sha1 != new_ie.text_sha1:
307
delta.modified.append((new_path, fid))
309
delta.modified.sort()
243
# If we got to here, we haven't written out the output of diff
247
# returns 1 if files differ; that's OK
249
msg = 'signal %d' % (-rc)
251
msg = 'exit code %d' % rc
253
raise errors.BzrError('external diff failed with %s; command: %r'
258
oldtmpf.close() # and delete
260
# Clean up. Warn in case the files couldn't be deleted
261
# (in case windows still holds the file open, but not
262
# if the files have already been deleted)
264
os.remove(old_abspath)
266
if e.errno not in (errno.ENOENT,):
267
warning('Failed to delete temporary file: %s %s',
270
os.remove(new_abspath)
272
if e.errno not in (errno.ENOENT,):
273
warning('Failed to delete temporary file: %s %s',
277
@deprecated_function(one_zero)
278
def diff_cmd_helper(tree, specific_files, external_diff_options,
279
old_revision_spec=None, new_revision_spec=None,
281
old_label='a/', new_label='b/'):
282
"""Helper for cmd_diff.
287
:param specific_files:
288
The specific files to compare, or None
290
:param external_diff_options:
291
If non-None, run an external diff, and pass it these options
293
:param old_revision_spec:
294
If None, use basis tree as old revision, otherwise use the tree for
295
the specified revision.
297
:param new_revision_spec:
298
If None, use working tree as new revision, otherwise use the tree for
299
the specified revision.
301
:param revision_specs:
302
Zero, one or two RevisionSpecs from the command line, saying what revisions
303
to compare. This can be passed as an alternative to the old_revision_spec
304
and new_revision_spec parameters.
306
The more general form is show_diff_trees(), where the caller
307
supplies any two trees.
310
# TODO: perhaps remove the old parameters old_revision_spec and
311
# new_revision_spec, since this is only really for use from cmd_diff and
312
# it now always passes through a sequence of revision_specs -- mbp
317
revision = spec.in_store(tree.branch)
319
revision = spec.in_store(None)
320
revision_id = revision.rev_id
321
branch = revision.branch
322
return branch.repository.revision_tree(revision_id)
324
if revision_specs is not None:
325
assert (old_revision_spec is None
326
and new_revision_spec is None)
327
if len(revision_specs) > 0:
328
old_revision_spec = revision_specs[0]
329
if len(revision_specs) > 1:
330
new_revision_spec = revision_specs[1]
332
if old_revision_spec is None:
333
old_tree = tree.basis_tree()
335
old_tree = spec_tree(old_revision_spec)
337
if (new_revision_spec is None
338
or new_revision_spec.spec is None):
341
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)
354
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
355
"""Get the trees and specific files to diff given a list of paths.
357
This method works out the trees to be diff'ed and the files of
358
interest within those trees.
361
the list of arguments passed to the diff command
362
:param revision_specs:
363
Zero, one or two RevisionSpecs from the diff command line,
364
saying what revisions to compare.
366
The url of the old branch or tree. If None, the tree to use is
367
taken from the first path, if any, or the current working tree.
369
The url of the new branch or tree. If None, the tree to use is
370
taken from the first path, if any, or the current working tree.
372
a tuple of (old_tree, new_tree, specific_files, extra_trees) where
373
extra_trees is a sequence of additional trees to search in for
376
# Get the old and new revision specs
377
old_revision_spec = None
378
new_revision_spec = None
379
if revision_specs is not None:
380
if len(revision_specs) > 0:
381
old_revision_spec = revision_specs[0]
383
old_url = old_revision_spec.get_branch()
384
if len(revision_specs) > 1:
385
new_revision_spec = revision_specs[1]
387
new_url = new_revision_spec.get_branch()
390
make_paths_wt_relative = True
391
consider_relpath = True
392
if path_list is None or len(path_list) == 0:
393
# If no path is given, the current working tree is used
394
default_location = u'.'
395
consider_relpath = False
396
elif old_url is not None and new_url is not None:
397
other_paths = path_list
398
make_paths_wt_relative = False
400
default_location = path_list[0]
401
other_paths = path_list[1:]
403
# Get the old location
406
old_url = default_location
407
working_tree, branch, relpath = \
408
bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
409
if consider_relpath and relpath != '':
410
specific_files.append(relpath)
411
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
413
# Get the new location
415
new_url = default_location
416
if new_url != old_url:
417
working_tree, branch, relpath = \
418
bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
419
if consider_relpath and relpath != '':
420
specific_files.append(relpath)
421
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
422
basis_is_default=working_tree is None)
424
# Get the specific files (all files is None, no files is [])
425
if make_paths_wt_relative and working_tree is not None:
426
other_paths = _relative_paths_in_tree(working_tree, other_paths)
427
specific_files.extend(other_paths)
428
if len(specific_files) == 0:
429
specific_files = None
431
# Get extra trees that ought to be searched for file-ids
433
if working_tree is not None and working_tree not in (old_tree, new_tree):
434
extra_trees = (working_tree,)
435
return old_tree, new_tree, specific_files, extra_trees
438
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
439
if branch is None and tree is not None:
441
if spec is None or spec.spec is None:
444
return tree.basis_tree()
446
return branch.basis_tree()
449
revision = spec.in_store(branch)
450
revision_id = revision.rev_id
451
rev_branch = revision.branch
452
return rev_branch.repository.revision_tree(revision_id)
455
def _relative_paths_in_tree(tree, paths):
456
"""Get the relative paths within a working tree.
458
Each path may be either an absolute path or a path relative to the
459
current working directory.
462
for filename in paths:
464
result.append(tree.relpath(osutils.dereference_path(filename)))
465
except errors.PathNotChild:
466
raise errors.BzrCommandError("Files are in different branches")
470
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
471
external_diff_options=None,
472
old_label='a/', new_label='b/',
474
path_encoding='utf8',
476
"""Show in text form the changes from one tree to another.
482
Include only changes to these files - None for all changes.
484
external_diff_options
485
If set, use an external GNU diff and pass these options.
488
If set, more Trees to use for looking up file ids
491
If set, the path will be encoded as specified, otherwise is supposed
496
if extra_trees is not None:
497
for tree in extra_trees:
501
differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
503
external_diff_options,
504
old_label, new_label, using)
505
return differ.show_diff(specific_files, extra_trees)
508
if extra_trees is not None:
509
for tree in extra_trees:
515
def _patch_header_date(tree, file_id, path):
516
"""Returns a timestamp suitable for use in a patch header."""
517
mtime = tree.get_file_mtime(file_id, path)
518
assert mtime is not None, \
519
"got an mtime of None for file-id %s, path %s in tree %s" % (
521
return timestamp.format_patch_date(mtime)
524
def _raise_if_nonexistent(paths, old_tree, new_tree):
525
"""Complain if paths are not in either inventory or tree.
527
It's OK with the files exist in either tree's inventory, or
528
if they exist in the tree but are not versioned.
530
This can be used by operations such as bzr status that can accept
531
unknown or ignored files.
533
mutter("check paths: %r", paths)
536
s = old_tree.filter_unversioned_files(paths)
537
s = new_tree.filter_unversioned_files(s)
538
s = [path for path in s if not new_tree.has_filename(path)]
540
raise errors.PathsDoNotExist(sorted(s))
543
def get_prop_change(meta_modified):
545
return " (properties changed)"
550
class DiffPath(object):
551
"""Base type for command object that compare files"""
553
# The type or contents of the file were unsuitable for diffing
554
CANNOT_DIFF = 'CANNOT_DIFF'
555
# The file has changed in a semantic way
557
# The file content may have changed, but there is no semantic change
558
UNCHANGED = 'UNCHANGED'
560
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
563
:param old_tree: The tree to show as the old tree in the comparison
564
:param new_tree: The tree to show as new in the comparison
565
:param to_file: The file to write comparison data to
566
:param path_encoding: The character encoding to write paths in
568
self.old_tree = old_tree
569
self.new_tree = new_tree
570
self.to_file = to_file
571
self.path_encoding = path_encoding
577
def from_diff_tree(klass, diff_tree):
578
return klass(diff_tree.old_tree, diff_tree.new_tree,
579
diff_tree.to_file, diff_tree.path_encoding)
582
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
583
for file_differ in differs:
584
result = file_differ.diff(file_id, old_path, new_path, old_kind,
586
if result is not DiffPath.CANNOT_DIFF:
589
return DiffPath.CANNOT_DIFF
592
class DiffKindChange(object):
593
"""Special differ for file kind changes.
595
Represents kind change as deletion + creation. Uses the other differs
598
def __init__(self, differs):
599
self.differs = differs
605
def from_diff_tree(klass, diff_tree):
606
return klass(diff_tree.differs)
608
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
609
"""Perform comparison
611
:param file_id: The file_id of the file to compare
612
:param old_path: Path of the file in the old tree
613
:param new_path: Path of the file in the new tree
614
:param old_kind: Old file-kind of the file
615
:param new_kind: New file-kind of the file
617
if None in (old_kind, new_kind):
618
return DiffPath.CANNOT_DIFF
619
result = DiffPath._diff_many(self.differs, file_id, old_path,
620
new_path, old_kind, None)
621
if result is DiffPath.CANNOT_DIFF:
623
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
627
class DiffDirectory(DiffPath):
629
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
630
"""Perform comparison between two directories. (dummy)
633
if 'directory' not in (old_kind, new_kind):
634
return self.CANNOT_DIFF
635
if old_kind not in ('directory', None):
636
return self.CANNOT_DIFF
637
if new_kind not in ('directory', None):
638
return self.CANNOT_DIFF
642
class DiffSymlink(DiffPath):
644
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
645
"""Perform comparison between two symlinks
647
:param file_id: The file_id of the file to compare
648
:param old_path: Path of the file in the old tree
649
:param new_path: Path of the file in the new tree
650
:param old_kind: Old file-kind of the file
651
:param new_kind: New file-kind of the file
653
if 'symlink' not in (old_kind, new_kind):
654
return self.CANNOT_DIFF
655
if old_kind == 'symlink':
656
old_target = self.old_tree.get_symlink_target(file_id)
657
elif old_kind is None:
660
return self.CANNOT_DIFF
661
if new_kind == 'symlink':
662
new_target = self.new_tree.get_symlink_target(file_id)
663
elif new_kind is None:
666
return self.CANNOT_DIFF
667
return self.diff_symlink(old_target, new_target)
669
def diff_symlink(self, old_target, new_target):
670
if old_target is None:
671
self.to_file.write('=== target is %r\n' % new_target)
672
elif new_target is None:
673
self.to_file.write('=== target was %r\n' % old_target)
675
self.to_file.write('=== target changed %r => %r\n' %
676
(old_target, new_target))
680
class DiffText(DiffPath):
682
# GNU Patch uses the epoch date to detect files that are being added
683
# or removed in a diff.
684
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
686
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
687
old_label='', new_label='', text_differ=internal_diff):
688
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
689
self.text_differ = text_differ
690
self.old_label = old_label
691
self.new_label = new_label
692
self.path_encoding = path_encoding
694
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
695
"""Compare two files in unified diff format
697
:param file_id: The file_id of the file to compare
698
:param old_path: Path of the file in the old tree
699
:param new_path: Path of the file in the new tree
700
:param old_kind: Old file-kind of the file
701
:param new_kind: New file-kind of the file
703
if 'file' not in (old_kind, new_kind):
704
return self.CANNOT_DIFF
705
from_file_id = to_file_id = file_id
706
if old_kind == 'file':
707
old_date = _patch_header_date(self.old_tree, file_id, old_path)
708
elif old_kind is None:
709
old_date = self.EPOCH_DATE
712
return self.CANNOT_DIFF
713
if new_kind == 'file':
714
new_date = _patch_header_date(self.new_tree, file_id, new_path)
715
elif new_kind is None:
716
new_date = self.EPOCH_DATE
719
return self.CANNOT_DIFF
720
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
721
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
722
return self.diff_text(from_file_id, to_file_id, from_label, to_label)
724
def diff_text(self, from_file_id, to_file_id, from_label, to_label):
725
"""Diff the content of given files in two trees
727
:param from_file_id: The id of the file in the from tree. If None,
728
the file is not present in the from tree.
729
:param to_file_id: The id of the file in the to tree. This may refer
730
to a different file from from_file_id. If None,
731
the file is not present in the to tree.
733
def _get_text(tree, file_id):
734
if file_id is not None:
735
return tree.get_file(file_id).readlines()
739
from_text = _get_text(self.old_tree, from_file_id)
740
to_text = _get_text(self.new_tree, to_file_id)
741
self.text_differ(from_label, from_text, to_label, to_text,
743
except errors.BinaryFile:
745
("Binary files %s and %s differ\n" %
746
(from_label, to_label)).encode(self.path_encoding))
750
class DiffFromTool(DiffPath):
752
def __init__(self, command_template, old_tree, new_tree, to_file,
753
path_encoding='utf-8'):
754
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
755
self.command_template = command_template
756
self._root = tempfile.mkdtemp(prefix='bzr-diff-')
759
def from_string(klass, command_string, old_tree, new_tree, to_file,
760
path_encoding='utf-8'):
761
command_template = commands.shlex_split_unicode(command_string)
762
command_template.extend(['%(old_path)s', '%(new_path)s'])
763
return klass(command_template, old_tree, new_tree, to_file,
767
def make_from_diff_tree(klass, command_string):
768
def from_diff_tree(diff_tree):
769
return klass.from_string(command_string, diff_tree.old_tree,
770
diff_tree.new_tree, diff_tree.to_file)
771
return from_diff_tree
773
def _get_command(self, old_path, new_path):
774
my_map = {'old_path': old_path, 'new_path': new_path}
775
return [t % my_map for t in self.command_template]
777
def _execute(self, old_path, new_path):
778
command = self._get_command(old_path, new_path)
780
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
783
if e.errno == errno.ENOENT:
784
raise errors.ExecutableMissing(command[0])
787
self.to_file.write(proc.stdout.read())
790
def _try_symlink_root(self, tree, prefix):
791
if not (getattr(tree, 'abspath', None) is not None
792
and osutils.has_symlinks()):
795
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
797
if e.errno != errno.EEXIST:
801
def _write_file(self, file_id, tree, prefix, relpath):
802
full_path = osutils.pathjoin(self._root, prefix, relpath)
803
if self._try_symlink_root(tree, prefix):
805
parent_dir = osutils.dirname(full_path)
807
os.makedirs(parent_dir)
809
if e.errno != errno.EEXIST:
811
source = tree.get_file(file_id, relpath)
813
target = open(full_path, 'wb')
815
osutils.pumpfile(source, target)
820
osutils.make_readonly(full_path)
821
mtime = tree.get_file_mtime(file_id)
822
os.utime(full_path, (mtime, mtime))
825
def _prepare_files(self, file_id, old_path, new_path):
826
old_disk_path = self._write_file(file_id, self.old_tree, 'old',
828
new_disk_path = self._write_file(file_id, self.new_tree, 'new',
830
return old_disk_path, new_disk_path
833
osutils.rmtree(self._root)
835
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
836
if (old_kind, new_kind) != ('file', 'file'):
837
return DiffPath.CANNOT_DIFF
838
self._prepare_files(file_id, old_path, new_path)
839
self._execute(osutils.pathjoin('old', old_path),
840
osutils.pathjoin('new', new_path))
843
class DiffTree(object):
844
"""Provides textual representations of the difference between two trees.
846
A DiffTree examines two trees and where a file-id has altered
847
between them, generates a textual representation of the difference.
848
DiffTree uses a sequence of DiffPath objects which are each
849
given the opportunity to handle a given altered fileid. The list
850
of DiffPath objects can be extended globally by appending to
851
DiffTree.diff_factories, or for a specific diff operation by
852
supplying the extra_factories option to the appropriate method.
855
# list of factories that can provide instances of DiffPath objects
856
# may be extended by plugins.
857
diff_factories = [DiffSymlink.from_diff_tree,
858
DiffDirectory.from_diff_tree]
860
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
861
diff_text=None, extra_factories=None):
864
:param old_tree: Tree to show as old in the comparison
865
:param new_tree: Tree to show as new in the comparison
866
:param to_file: File to write comparision to
867
:param path_encoding: Character encoding to write paths in
868
:param diff_text: DiffPath-type object to use as a last resort for
870
:param extra_factories: Factories of DiffPaths to try before any other
872
if diff_text is None:
873
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
874
'', '', internal_diff)
875
self.old_tree = old_tree
876
self.new_tree = new_tree
877
self.to_file = to_file
878
self.path_encoding = path_encoding
880
if extra_factories is not None:
881
self.differs.extend(f(self) for f in extra_factories)
882
self.differs.extend(f(self) for f in self.diff_factories)
883
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
886
def from_trees_options(klass, old_tree, new_tree, to_file,
887
path_encoding, external_diff_options, old_label,
889
"""Factory for producing a DiffTree.
891
Designed to accept options used by show_diff_trees.
892
:param old_tree: The tree to show as old in the comparison
893
:param new_tree: The tree to show as new in the comparison
894
:param to_file: File to write comparisons to
895
:param path_encoding: Character encoding to use for writing paths
896
:param external_diff_options: If supplied, use the installed diff
897
binary to perform file comparison, using supplied options.
898
:param old_label: Prefix to use for old file labels
899
:param new_label: Prefix to use for new file labels
900
:param using: Commandline to use to invoke an external diff tool
902
if using is not None:
903
extra_factories = [DiffFromTool.make_from_diff_tree(using)]
906
if external_diff_options:
907
assert isinstance(external_diff_options, basestring)
908
opts = external_diff_options.split()
909
def diff_file(olab, olines, nlab, nlines, to_file):
910
external_diff(olab, olines, nlab, nlines, to_file, opts)
912
diff_file = internal_diff
913
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
914
old_label, new_label, diff_file)
915
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
918
def show_diff(self, specific_files, extra_trees=None):
919
"""Write tree diff to self.to_file
921
:param sepecific_files: the specific files to compare (recursive)
922
:param extra_trees: extra trees to use for mapping paths to file_ids
925
return self._show_diff(specific_files, extra_trees)
927
for differ in self.differs:
930
def _show_diff(self, specific_files, extra_trees):
931
# TODO: Generation of pseudo-diffs for added/deleted files could
932
# be usefully made into a much faster special case.
933
iterator = self.new_tree.iter_changes(self.old_tree,
934
specific_files=specific_files,
935
extra_trees=extra_trees,
936
require_versioned=True)
938
def changes_key(change):
939
old_path, new_path = change[1]
944
def get_encoded_path(path):
946
return path.encode(self.path_encoding, "replace")
947
for (file_id, paths, changed_content, versioned, parent, name, kind,
948
executable) in sorted(iterator, key=changes_key):
949
if parent == (None, None):
951
oldpath, newpath = paths
952
oldpath_encoded = get_encoded_path(paths[0])
953
newpath_encoded = get_encoded_path(paths[1])
954
old_present = (kind[0] is not None and versioned[0])
955
new_present = (kind[1] is not None and versioned[1])
956
renamed = (parent[0], name[0]) != (parent[1], name[1])
957
prop_str = get_prop_change(executable[0] != executable[1])
958
if (old_present, new_present) == (True, False):
959
self.to_file.write("=== removed %s '%s'\n" %
960
(kind[0], oldpath_encoded))
962
elif (old_present, new_present) == (False, True):
963
self.to_file.write("=== added %s '%s'\n" %
964
(kind[1], newpath_encoded))
967
self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
968
(kind[0], oldpath_encoded, newpath_encoded, prop_str))
970
# if it was produced by iter_changes, it must be
971
# modified *somehow*, either content or execute bit.
972
self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
973
newpath_encoded, prop_str))
975
self.diff(file_id, oldpath, newpath)
981
def diff(self, file_id, old_path, new_path):
982
"""Perform a diff of a single file
984
:param file_id: file-id of the file
985
:param old_path: The path of the file in the old tree
986
:param new_path: The path of the file in the new tree
989
old_kind = self.old_tree.kind(file_id)
990
except (errors.NoSuchId, errors.NoSuchFile):
993
new_kind = self.new_tree.kind(file_id)
994
except (errors.NoSuchId, errors.NoSuchFile):
997
result = DiffPath._diff_many(self.differs, file_id, old_path,
998
new_path, old_kind, new_kind)
999
if result is DiffPath.CANNOT_DIFF:
1000
error_path = new_path
1001
if error_path is None:
1002
error_path = old_path
1003
raise errors.NoDiffFound(error_path)