94
272
oldtmpf.writelines(oldlines)
95
273
newtmpf.writelines(newlines)
100
278
if not diff_opts:
280
if sys.platform == 'win32':
281
# Popen doesn't do the proper encoding for external commands
282
# Since we are dealing with an ANSI api, use mbcs encoding
283
old_label = old_label.encode('mbcs')
284
new_label = new_label.encode('mbcs')
102
285
diffcmd = ['diff',
103
'--label', old_filename+'\t',
105
'--label', new_filename+'\t',
108
# diff only allows one style to be specified; they don't override.
109
# note that some of these take optargs, and the optargs can be
110
# directly appended to the options.
111
# this is only an approximate parser; it doesn't properly understand
113
for s in ['-c', '-u', '-C', '-U',
118
'-y', '--side-by-side',
286
'--label', old_label,
288
'--label', new_label,
293
diff_opts = default_style_unified(diff_opts)
130
296
diffcmd.extend(diff_opts)
132
rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
134
if rc != 0 and rc != 1:
298
pipe = _spawn_external_diff(diffcmd, capture_errors=True)
299
out, err = pipe.communicate()
302
# internal_diff() adds a trailing newline, add one here for consistency
305
# 'diff' gives retcode == 2 for all sorts of errors
306
# one of those is 'Binary files differ'.
307
# Bad options could also be the problem.
308
# 'Binary files' is not a real error, so we suppress that error.
311
# Since we got here, we want to make sure to give an i18n error
312
pipe = _spawn_external_diff(diffcmd, capture_errors=False)
313
out, err = pipe.communicate()
315
# Write out the new i18n diff response
316
to_file.write(out + b'\n')
317
if pipe.returncode != 2:
318
raise errors.BzrError(
319
'external diff failed with exit code 2'
320
' when run with LANG=C and LC_ALL=C,'
321
' but not when run natively: %r' % (diffcmd,))
323
first_line = lang_c_out.split(b'\n', 1)[0]
324
# Starting with diffutils 2.8.4 the word "binary" was dropped.
325
m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
327
raise errors.BzrError('external diff failed with exit code 2;'
328
' command: %r' % (diffcmd,))
330
# Binary files differ, just return
333
# If we got to here, we haven't written out the output of diff
135
337
# returns 1 if files differ; that's OK
137
339
msg = 'signal %d' % (-rc)
139
341
msg = 'exit code %d' % rc
141
raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
343
raise errors.BzrError('external diff failed with %s; command: %r'
143
347
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.
351
# Warn in case the file couldn't be deleted (in case windows still
352
# holds the file open, but not if the files have already been
357
if e.errno not in (errno.ENOENT,):
358
warning('Failed to delete temporary file: %s %s', path, e)
364
def get_trees_and_branches_to_diff_locked(
365
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
366
"""Get the trees and specific files to diff given a list of paths.
368
This method works out the trees to be diff'ed and the files of
369
interest within those trees.
372
the list of arguments passed to the diff command
373
:param revision_specs:
374
Zero, one or two RevisionSpecs from the diff command line,
375
saying what revisions to compare.
377
The url of the old branch or tree. If None, the tree to use is
378
taken from the first path, if any, or the current working tree.
380
The url of the new branch or tree. If None, the tree to use is
381
taken from the first path, if any, or the current working tree.
383
a callable like Command.add_cleanup. get_trees_and_branches_to_diff
384
will register cleanups that must be run to unlock the trees, etc.
386
if True and a view is set, apply the view or check that the paths
389
a tuple of (old_tree, new_tree, old_branch, new_branch,
390
specific_files, extra_trees) where extra_trees is a sequence of
391
additional trees to search in for file-ids. The trees and branches
392
will be read-locked until the cleanups registered via the add_cleanup
395
# Get the old and new revision specs
396
old_revision_spec = None
397
new_revision_spec = None
398
if revision_specs is not None:
399
if len(revision_specs) > 0:
400
old_revision_spec = revision_specs[0]
402
old_url = old_revision_spec.get_branch()
403
if len(revision_specs) > 1:
404
new_revision_spec = revision_specs[1]
406
new_url = new_revision_spec.get_branch()
167
if from_spec is None:
168
old_tree = b.bzrdir.open_workingtree()
170
old_tree = old_tree = old_tree.basis_tree()
409
make_paths_wt_relative = True
410
consider_relpath = True
411
if path_list is None or len(path_list) == 0:
412
# If no path is given, the current working tree is used
413
default_location = u'.'
414
consider_relpath = False
415
elif old_url is not None and new_url is not None:
416
other_paths = path_list
417
make_paths_wt_relative = False
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()
419
default_location = path_list[0]
420
other_paths = path_list[1:]
422
def lock_tree_or_branch(wt, br):
425
add_cleanup(wt.unlock)
428
add_cleanup(br.unlock)
430
# Get the old location
433
old_url = default_location
434
working_tree, branch, relpath = \
435
controldir.ControlDir.open_containing_tree_or_branch(old_url)
436
lock_tree_or_branch(working_tree, branch)
437
if consider_relpath and relpath != '':
438
if working_tree is not None and apply_view:
439
views.check_path_in_view(working_tree, relpath)
440
specific_files.append(relpath)
441
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
444
# Get the new location
446
new_url = default_location
447
if new_url != old_url:
448
working_tree, branch, relpath = \
449
controldir.ControlDir.open_containing_tree_or_branch(new_url)
450
lock_tree_or_branch(working_tree, branch)
451
if consider_relpath and relpath != '':
452
if working_tree is not None and apply_view:
453
views.check_path_in_view(working_tree, relpath)
454
specific_files.append(relpath)
455
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
456
basis_is_default=working_tree is None)
459
# Get the specific files (all files is None, no files is [])
460
if make_paths_wt_relative and working_tree is not None:
461
other_paths = working_tree.safe_relpath_files(
463
apply_view=apply_view)
464
specific_files.extend(other_paths)
465
if len(specific_files) == 0:
466
specific_files = None
467
if (working_tree is not None and working_tree.supports_views() and
469
view_files = working_tree.views.lookup_view()
471
specific_files = view_files
472
view_str = views.view_display_str(view_files)
473
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
475
# Get extra trees that ought to be searched for file-ids
477
if working_tree is not None and working_tree not in (old_tree, new_tree):
478
extra_trees = (working_tree,)
479
return (old_tree, new_tree, old_branch, new_branch,
480
specific_files, extra_trees)
483
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
484
if branch is None and tree is not None:
486
if spec is None or spec.spec is None:
489
return tree.basis_tree()
491
return branch.basis_tree()
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)
186
def diff_cmd_helper(tree, specific_files, external_diff_options,
187
old_revision_spec=None, new_revision_spec=None):
188
"""Helper for cmd_diff.
194
The specific files to compare, or None
196
external_diff_options
197
If non-None, run an external diff, and pass it these options
200
If None, use basis tree as old revision, otherwise use the tree for
201
the specified revision.
204
If None, use working tree as new revision, otherwise use the tree for
205
the specified revision.
207
The more general form is show_diff_trees(), where the caller
208
supplies any two trees.
213
revision_id = spec.in_store(tree.branch).rev_id
214
return tree.branch.repository.revision_tree(revision_id)
215
if old_revision_spec is None:
216
old_tree = tree.basis_tree()
218
old_tree = spec_tree(old_revision_spec)
220
if new_revision_spec is None:
223
new_tree = spec_tree(new_revision_spec)
225
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
226
external_diff_options)
494
return spec.as_tree(branch)
229
497
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
230
external_diff_options=None):
498
external_diff_options=None,
499
old_label='a/', new_label='b/',
501
path_encoding='utf8',
504
context=DEFAULT_CONTEXT_AMOUNT):
231
505
"""Show in text form the changes from one tree to another.
234
If set, include only changes to these files.
236
external_diff_options
237
If set, use an external GNU diff and pass these options.
507
:param to_file: The output stream.
508
:param specific_files: Include only changes to these files - None for all
510
:param external_diff_options: If set, use an external GNU diff and pass
512
:param extra_trees: If set, more Trees to use for looking up file ids
513
:param path_encoding: If set, the path will be encoded as specified,
514
otherwise is supposed to be utf8
515
:param format_cls: Formatter class (DiffTree subclass)
518
context = DEFAULT_CONTEXT_AMOUNT
519
if format_cls is None:
520
format_cls = DiffTree
521
with old_tree.lock_read():
522
if extra_trees is not None:
523
for tree in extra_trees:
241
525
new_tree.lock_read()
243
return _show_diff_trees(old_tree, new_tree, to_file,
244
specific_files, external_diff_options)
527
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
529
external_diff_options,
530
old_label, new_label, using,
531
context_lines=context)
532
return differ.show_diff(specific_files, extra_trees)
246
534
new_tree.unlock()
251
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.
264
# TODO: Generation of pseudo-diffs for added/deleted files could
265
# be usefully made into a much faster special case.
267
_raise_if_doubly_unversioned(specific_files, old_tree, new_tree)
269
if external_diff_options:
270
assert isinstance(external_diff_options, basestring)
271
opts = external_diff_options.split()
272
def diff_file(olab, olines, nlab, nlines, to_file):
273
external_diff(olab, olines, nlab, nlines, to_file, opts)
275
diff_file = internal_diff
277
delta = compare_trees(old_tree, new_tree, want_unchanged=False,
278
specific_files=specific_files)
281
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)
286
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,
292
for (old_path, new_path, file_id, kind,
293
text_modified, meta_modified) in delta.renamed:
295
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,
300
text_modified, kind, to_file, diff_file)
301
for path, file_id, kind, text_modified, meta_modified in delta.modified:
303
prop_str = get_prop_change(meta_modified)
304
print >>to_file, '=== modified %s %r%s' % (kind, old_label + path,
307
_maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
308
new_label, path, new_tree,
309
True, kind, to_file, diff_file)
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:
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))
325
def get_prop_change(meta_modified):
327
return " (properties changed)"
332
def _maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
333
new_label, new_path, new_tree, text_modified,
334
kind, to_file, diff_file):
336
new_entry = new_tree.inventory[file_id]
337
old_tree.inventory[file_id].diff(diff_file,
338
old_label + old_path, old_tree,
339
new_label + new_path, new_entry,
535
if extra_trees is not None:
536
for tree in extra_trees:
540
def _patch_header_date(tree, path):
541
"""Returns a timestamp suitable for use in a patch header."""
543
mtime = tree.get_file_mtime(path)
544
except FileTimestampUnavailable:
546
return timestamp.format_patch_date(mtime)
549
def get_executable_change(old_is_x, new_is_x):
550
descr = {True: b"+x", False: b"-x", None: b"??"}
551
if old_is_x != new_is_x:
552
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
557
class DiffPath(object):
558
"""Base type for command object that compare files"""
560
# The type or contents of the file were unsuitable for diffing
561
CANNOT_DIFF = 'CANNOT_DIFF'
562
# The file has changed in a semantic way
564
# The file content may have changed, but there is no semantic change
565
UNCHANGED = 'UNCHANGED'
567
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
570
:param old_tree: The tree to show as the old tree in the comparison
571
:param new_tree: The tree to show as new in the comparison
572
:param to_file: The file to write comparison data to
573
:param path_encoding: The character encoding to write paths in
575
self.old_tree = old_tree
576
self.new_tree = new_tree
577
self.to_file = to_file
578
self.path_encoding = path_encoding
584
def from_diff_tree(klass, diff_tree):
585
return klass(diff_tree.old_tree, diff_tree.new_tree,
586
diff_tree.to_file, diff_tree.path_encoding)
589
def _diff_many(differs, old_path, new_path, old_kind, new_kind):
590
for file_differ in differs:
591
result = file_differ.diff(old_path, new_path, old_kind, new_kind)
592
if result is not DiffPath.CANNOT_DIFF:
595
return DiffPath.CANNOT_DIFF
598
class DiffKindChange(object):
599
"""Special differ for file kind changes.
601
Represents kind change as deletion + creation. Uses the other differs
605
def __init__(self, differs):
606
self.differs = differs
612
def from_diff_tree(klass, diff_tree):
613
return klass(diff_tree.differs)
615
def diff(self, old_path, new_path, old_kind, new_kind):
616
"""Perform comparison
618
:param old_path: Path of the file in the old tree
619
:param new_path: Path of the file in the new tree
620
:param old_kind: Old file-kind of the file
621
:param new_kind: New file-kind of the file
623
if None in (old_kind, new_kind):
624
return DiffPath.CANNOT_DIFF
625
result = DiffPath._diff_many(
626
self.differs, old_path, new_path, old_kind, None)
627
if result is DiffPath.CANNOT_DIFF:
629
return DiffPath._diff_many(
630
self.differs, old_path, new_path, None, new_kind)
633
class DiffTreeReference(DiffPath):
635
def diff(self, old_path, new_path, old_kind, new_kind):
636
"""Perform comparison between two tree references. (dummy)
639
if 'tree-reference' not in (old_kind, new_kind):
640
return self.CANNOT_DIFF
641
if old_kind not in ('tree-reference', None):
642
return self.CANNOT_DIFF
643
if new_kind not in ('tree-reference', None):
644
return self.CANNOT_DIFF
648
class DiffDirectory(DiffPath):
650
def diff(self, old_path, new_path, old_kind, new_kind):
651
"""Perform comparison between two directories. (dummy)
654
if 'directory' not in (old_kind, new_kind):
655
return self.CANNOT_DIFF
656
if old_kind not in ('directory', None):
657
return self.CANNOT_DIFF
658
if new_kind not in ('directory', None):
659
return self.CANNOT_DIFF
663
class DiffSymlink(DiffPath):
665
def diff(self, old_path, new_path, old_kind, new_kind):
666
"""Perform comparison between two symlinks
668
:param old_path: Path of the file in the old tree
669
:param new_path: Path of the file in the new tree
670
:param old_kind: Old file-kind of the file
671
:param new_kind: New file-kind of the file
673
if 'symlink' not in (old_kind, new_kind):
674
return self.CANNOT_DIFF
675
if old_kind == 'symlink':
676
old_target = self.old_tree.get_symlink_target(old_path)
677
elif old_kind is None:
680
return self.CANNOT_DIFF
681
if new_kind == 'symlink':
682
new_target = self.new_tree.get_symlink_target(new_path)
683
elif new_kind is None:
686
return self.CANNOT_DIFF
687
return self.diff_symlink(old_target, new_target)
689
def diff_symlink(self, old_target, new_target):
690
if old_target is None:
691
self.to_file.write(b'=== target is \'%s\'\n' %
692
new_target.encode(self.path_encoding, 'replace'))
693
elif new_target is None:
694
self.to_file.write(b'=== target was \'%s\'\n' %
695
old_target.encode(self.path_encoding, 'replace'))
697
self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
698
(old_target.encode(self.path_encoding, 'replace'),
699
new_target.encode(self.path_encoding, 'replace')))
703
class DiffText(DiffPath):
705
# GNU Patch uses the epoch date to detect files that are being added
706
# or removed in a diff.
707
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
709
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
710
old_label='', new_label='', text_differ=internal_diff,
711
context_lines=DEFAULT_CONTEXT_AMOUNT):
712
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
713
self.text_differ = text_differ
714
self.old_label = old_label
715
self.new_label = new_label
716
self.path_encoding = path_encoding
717
self.context_lines = context_lines
719
def diff(self, old_path, new_path, old_kind, new_kind):
720
"""Compare two files in unified diff format
722
:param old_path: Path of the file in the old tree
723
:param new_path: Path of the file in the new tree
724
:param old_kind: Old file-kind of the file
725
:param new_kind: New file-kind of the file
727
if 'file' not in (old_kind, new_kind):
728
return self.CANNOT_DIFF
729
if old_kind == 'file':
730
old_date = _patch_header_date(self.old_tree, old_path)
731
elif old_kind is None:
732
old_date = self.EPOCH_DATE
734
return self.CANNOT_DIFF
735
if new_kind == 'file':
736
new_date = _patch_header_date(self.new_tree, new_path)
737
elif new_kind is None:
738
new_date = self.EPOCH_DATE
740
return self.CANNOT_DIFF
741
from_label = '%s%s\t%s' % (self.old_label, old_path,
743
to_label = '%s%s\t%s' % (self.new_label, new_path,
745
return self.diff_text(old_path, new_path, from_label, to_label)
747
def diff_text(self, from_path, to_path, from_label, to_label):
748
"""Diff the content of given files in two trees
750
:param from_path: The path in the from tree. If None,
751
the file is not present in the from tree.
752
:param to_path: The path in the to tree. This may refer
753
to a different file from from_path. If None,
754
the file is not present in the to tree.
756
def _get_text(tree, path):
760
return tree.get_file_lines(path)
761
except errors.NoSuchFile:
764
from_text = _get_text(self.old_tree, from_path)
765
to_text = _get_text(self.new_tree, to_path)
766
self.text_differ(from_label, from_text, to_label, to_text,
767
self.to_file, path_encoding=self.path_encoding,
768
context_lines=self.context_lines)
769
except errors.BinaryFile:
771
("Binary files %s%s and %s%s differ\n" %
772
(self.old_label, from_path, self.new_label, to_path)).encode(self.path_encoding, 'replace'))
776
class DiffFromTool(DiffPath):
778
def __init__(self, command_template, old_tree, new_tree, to_file,
779
path_encoding='utf-8'):
780
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
781
self.command_template = command_template
782
self._root = osutils.mkdtemp(prefix='brz-diff-')
785
def from_string(klass, command_template, old_tree, new_tree, to_file,
786
path_encoding='utf-8'):
787
return klass(command_template, old_tree, new_tree, to_file,
791
def make_from_diff_tree(klass, command_string, external_diff_options=None):
792
def from_diff_tree(diff_tree):
793
full_command_string = [command_string]
794
if external_diff_options is not None:
795
full_command_string += ' ' + external_diff_options
796
return klass.from_string(full_command_string, diff_tree.old_tree,
797
diff_tree.new_tree, diff_tree.to_file)
798
return from_diff_tree
800
def _get_command(self, old_path, new_path):
801
my_map = {'old_path': old_path, 'new_path': new_path}
802
command = [t.format(**my_map) for t in
803
self.command_template]
804
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
807
if isinstance(c, text_type):
808
command_encoded.append(c.encode('mbcs'))
810
command_encoded.append(c)
811
return command_encoded
815
def _execute(self, old_path, new_path):
816
command = self._get_command(old_path, new_path)
818
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
821
if e.errno == errno.ENOENT:
822
raise errors.ExecutableMissing(command[0])
825
self.to_file.write(proc.stdout.read())
829
def _try_symlink_root(self, tree, prefix):
830
if (getattr(tree, 'abspath', None) is None or
831
not osutils.host_os_dereferences_symlinks()):
834
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
836
if e.errno != errno.EEXIST:
842
"""Returns safe encoding for passing file path to diff tool"""
843
if sys.platform == 'win32':
846
# Don't fallback to 'utf-8' because subprocess may not be able to
847
# handle utf-8 correctly when locale is not utf-8.
848
return sys.getfilesystemencoding() or 'ascii'
850
def _is_safepath(self, path):
851
"""Return true if `path` may be able to pass to subprocess."""
854
return path == path.encode(fenc).decode(fenc)
858
def _safe_filename(self, prefix, relpath):
859
"""Replace unsafe character in `relpath` then join `self._root`,
860
`prefix` and `relpath`."""
862
# encoded_str.replace('?', '_') may break multibyte char.
863
# So we should encode, decode, then replace(u'?', u'_')
864
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
865
relpath_tmp = relpath_tmp.replace(u'?', u'_')
866
return osutils.pathjoin(self._root, prefix, relpath_tmp)
868
def _write_file(self, relpath, tree, prefix, force_temp=False,
870
if not force_temp and isinstance(tree, WorkingTree):
871
full_path = tree.abspath(relpath)
872
if self._is_safepath(full_path):
875
full_path = self._safe_filename(prefix, relpath)
876
if not force_temp and self._try_symlink_root(tree, prefix):
878
parent_dir = osutils.dirname(full_path)
880
os.makedirs(parent_dir)
882
if e.errno != errno.EEXIST:
884
source = tree.get_file(relpath)
886
with open(full_path, 'wb') as target:
887
osutils.pumpfile(source, target)
891
mtime = tree.get_file_mtime(relpath)
892
except FileTimestampUnavailable:
895
os.utime(full_path, (mtime, mtime))
897
osutils.make_readonly(full_path)
900
def _prepare_files(self, old_path, new_path, force_temp=False,
901
allow_write_new=False):
902
old_disk_path = self._write_file(
903
old_path, self.old_tree, 'old', force_temp)
904
new_disk_path = self._write_file(
905
new_path, self.new_tree, 'new', force_temp,
906
allow_write=allow_write_new)
907
return old_disk_path, new_disk_path
911
osutils.rmtree(self._root)
913
if e.errno != errno.ENOENT:
914
mutter("The temporary directory \"%s\" was not "
915
"cleanly removed: %s." % (self._root, e))
917
def diff(self, old_path, new_path, old_kind, new_kind):
918
if (old_kind, new_kind) != ('file', 'file'):
919
return DiffPath.CANNOT_DIFF
920
(old_disk_path, new_disk_path) = self._prepare_files(
922
self._execute(old_disk_path, new_disk_path)
924
def edit_file(self, old_path, new_path):
925
"""Use this tool to edit a file.
927
A temporary copy will be edited, and the new contents will be
930
:return: The new contents of the file.
932
old_abs_path, new_abs_path = self._prepare_files(
933
old_path, new_path, allow_write_new=True, force_temp=True)
934
command = self._get_command(old_abs_path, new_abs_path)
935
subprocess.call(command, cwd=self._root)
936
with open(new_abs_path, 'rb') as new_file:
937
return new_file.read()
940
class DiffTree(object):
941
"""Provides textual representations of the difference between two trees.
943
A DiffTree examines two trees and where a file-id has altered
944
between them, generates a textual representation of the difference.
945
DiffTree uses a sequence of DiffPath objects which are each
946
given the opportunity to handle a given altered fileid. The list
947
of DiffPath objects can be extended globally by appending to
948
DiffTree.diff_factories, or for a specific diff operation by
949
supplying the extra_factories option to the appropriate method.
952
# list of factories that can provide instances of DiffPath objects
953
# may be extended by plugins.
954
diff_factories = [DiffSymlink.from_diff_tree,
955
DiffDirectory.from_diff_tree,
956
DiffTreeReference.from_diff_tree]
958
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
959
diff_text=None, extra_factories=None):
962
:param old_tree: Tree to show as old in the comparison
963
:param new_tree: Tree to show as new in the comparison
964
:param to_file: File to write comparision to
965
:param path_encoding: Character encoding to write paths in
966
:param diff_text: DiffPath-type object to use as a last resort for
968
:param extra_factories: Factories of DiffPaths to try before any other
970
if diff_text is None:
971
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
972
'', '', internal_diff)
973
self.old_tree = old_tree
974
self.new_tree = new_tree
975
self.to_file = to_file
976
self.path_encoding = path_encoding
978
if extra_factories is not None:
979
self.differs.extend(f(self) for f in extra_factories)
980
self.differs.extend(f(self) for f in self.diff_factories)
981
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
984
def from_trees_options(klass, old_tree, new_tree, to_file,
985
path_encoding, external_diff_options, old_label,
986
new_label, using, context_lines):
987
"""Factory for producing a DiffTree.
989
Designed to accept options used by show_diff_trees.
991
:param old_tree: The tree to show as old in the comparison
992
:param new_tree: The tree to show as new in the comparison
993
:param to_file: File to write comparisons to
994
:param path_encoding: Character encoding to use for writing paths
995
:param external_diff_options: If supplied, use the installed diff
996
binary to perform file comparison, using supplied options.
997
:param old_label: Prefix to use for old file labels
998
:param new_label: Prefix to use for new file labels
999
:param using: Commandline to use to invoke an external diff tool
1001
if using is not None:
1002
extra_factories = [DiffFromTool.make_from_diff_tree(
1003
using, external_diff_options)]
1005
extra_factories = []
1006
if external_diff_options:
1007
opts = external_diff_options.split()
1009
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
1010
""":param path_encoding: not used but required
1011
to match the signature of internal_diff.
1013
external_diff(olab, olines, nlab, nlines, to_file, opts)
1015
diff_file = internal_diff
1016
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
1017
old_label, new_label, diff_file, context_lines=context_lines)
1018
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
1021
def show_diff(self, specific_files, extra_trees=None):
1022
"""Write tree diff to self.to_file
1024
:param specific_files: the specific files to compare (recursive)
1025
:param extra_trees: extra trees to use for mapping paths to file_ids
1028
return self._show_diff(specific_files, extra_trees)
1030
for differ in self.differs:
1033
def _show_diff(self, specific_files, extra_trees):
1034
# TODO: Generation of pseudo-diffs for added/deleted files could
1035
# be usefully made into a much faster special case.
1036
iterator = self.new_tree.iter_changes(self.old_tree,
1037
specific_files=specific_files,
1038
extra_trees=extra_trees,
1039
require_versioned=True)
1042
def changes_key(change):
1043
old_path, new_path = change[1]
1049
def get_encoded_path(path):
1050
if path is not None:
1051
return path.encode(self.path_encoding, "replace")
1052
for change in sorted(iterator, key=changes_key):
1053
# The root does not get diffed, and items with no known kind (that
1054
# is, missing) in both trees are skipped as well.
1055
if change.parent_id == (None, None) or change.kind == (None, None):
1057
if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
1059
'Ignoring "%s" as symlinks are not '
1060
'supported on this filesystem.' % (change.path[0],))
1062
oldpath, newpath = change.path
1063
oldpath_encoded = get_encoded_path(change.path[0])
1064
newpath_encoded = get_encoded_path(change.path[1])
1065
old_present = (change.kind[0] is not None and change.versioned[0])
1066
new_present = (change.kind[1] is not None and change.versioned[1])
1067
executable = change.executable
1069
renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
1071
properties_changed = []
1072
properties_changed.extend(
1073
get_executable_change(executable[0], executable[1]))
1075
if properties_changed:
1076
prop_str = b" (properties changed: %s)" % (
1077
b", ".join(properties_changed),)
1081
if (old_present, new_present) == (True, False):
1082
self.to_file.write(b"=== removed %s '%s'\n" %
1083
(kind[0].encode('ascii'), oldpath_encoded))
1085
elif (old_present, new_present) == (False, True):
1086
self.to_file.write(b"=== added %s '%s'\n" %
1087
(kind[1].encode('ascii'), newpath_encoded))
1090
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1091
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1093
# if it was produced by iter_changes, it must be
1094
# modified *somehow*, either content or execute bit.
1095
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1096
newpath_encoded, prop_str))
1097
if change.changed_content:
1098
self._diff(oldpath, newpath, kind[0], kind[1])
1104
def diff(self, old_path, new_path):
1105
"""Perform a diff of a single file
1107
:param old_path: The path of the file in the old tree
1108
:param new_path: The path of the file in the new tree
1110
if old_path is None:
1113
old_kind = self.old_tree.kind(old_path)
1114
if new_path is None:
1117
new_kind = self.new_tree.kind(new_path)
1118
self._diff(old_path, new_path, old_kind, new_kind)
1120
def _diff(self, old_path, new_path, old_kind, new_kind):
1121
result = DiffPath._diff_many(
1122
self.differs, old_path, new_path, old_kind, new_kind)
1123
if result is DiffPath.CANNOT_DIFF:
1124
error_path = new_path
1125
if error_path is None:
1126
error_path = old_path
1127
raise errors.NoDiffFound(error_path)
1130
format_registry = Registry()
1131
format_registry.register('default', DiffTree)