274
236
msg = 'signal %d' % (-rc)
276
238
msg = 'exit code %d' % rc
278
raise errors.BzrError('external diff failed with %s; command: %r'
240
raise errors.BzrError('external diff failed with %s; command: %r'
283
245
oldtmpf.close() # and delete
287
# Warn in case the file couldn't be deleted (in case windows still
288
# holds the file open, but not if the files have already been
293
if e.errno not in (errno.ENOENT,):
294
warning('Failed to delete temporary file: %s %s', path, e)
300
def get_trees_and_branches_to_diff_locked(
301
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
302
"""Get the trees and specific files to diff given a list of paths.
304
This method works out the trees to be diff'ed and the files of
305
interest within those trees.
308
the list of arguments passed to the diff command
309
:param revision_specs:
310
Zero, one or two RevisionSpecs from the diff command line,
311
saying what revisions to compare.
313
The url of the old branch or tree. If None, the tree to use is
314
taken from the first path, if any, or the current working tree.
316
The url of the new branch or tree. If None, the tree to use is
317
taken from the first path, if any, or the current working tree.
319
a callable like Command.add_cleanup. get_trees_and_branches_to_diff
320
will register cleanups that must be run to unlock the trees, etc.
322
if True and a view is set, apply the view or check that the paths
325
a tuple of (old_tree, new_tree, old_branch, new_branch,
326
specific_files, extra_trees) where extra_trees is a sequence of
327
additional trees to search in for file-ids. The trees and branches
328
will be read-locked until the cleanups registered via the add_cleanup
331
# Get the old and new revision specs
332
old_revision_spec = None
333
new_revision_spec = None
247
# Clean up. Warn in case the files couldn't be deleted
248
# (in case windows still holds the file open, but not
249
# if the files have already been deleted)
251
os.remove(old_abspath)
253
if e.errno not in (errno.ENOENT,):
254
warning('Failed to delete temporary file: %s %s',
257
os.remove(new_abspath)
259
if e.errno not in (errno.ENOENT,):
260
warning('Failed to delete temporary file: %s %s',
264
@deprecated_function(zero_eight)
265
def show_diff(b, from_spec, specific_files, external_diff_options=None,
266
revision2=None, output=None, b2=None):
267
"""Shortcut for showing the diff to the working tree.
269
Please use show_diff_trees instead.
275
None for 'basis tree', or otherwise the old revision to compare against.
277
The more general form is show_diff_trees(), where the caller
278
supplies any two trees.
283
if from_spec is None:
284
old_tree = b.bzrdir.open_workingtree()
286
old_tree = old_tree = old_tree.basis_tree()
288
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
290
if revision2 is None:
292
new_tree = b.bzrdir.open_workingtree()
294
new_tree = b2.bzrdir.open_workingtree()
296
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
298
return show_diff_trees(old_tree, new_tree, output, specific_files,
299
external_diff_options)
302
def diff_cmd_helper(tree, specific_files, external_diff_options,
303
old_revision_spec=None, new_revision_spec=None,
305
old_label='a/', new_label='b/'):
306
"""Helper for cmd_diff.
311
:param specific_files:
312
The specific files to compare, or None
314
:param external_diff_options:
315
If non-None, run an external diff, and pass it these options
317
:param old_revision_spec:
318
If None, use basis tree as old revision, otherwise use the tree for
319
the specified revision.
321
:param new_revision_spec:
322
If None, use working tree as new revision, otherwise use the tree for
323
the specified revision.
325
:param revision_specs:
326
Zero, one or two RevisionSpecs from the command line, saying what revisions
327
to compare. This can be passed as an alternative to the old_revision_spec
328
and new_revision_spec parameters.
330
The more general form is show_diff_trees(), where the caller
331
supplies any two trees.
334
# TODO: perhaps remove the old parameters old_revision_spec and
335
# new_revision_spec, since this is only really for use from cmd_diff and
336
# it now always passes through a sequence of revision_specs -- mbp
341
revision = spec.in_store(tree.branch)
343
revision = spec.in_store(None)
344
revision_id = revision.rev_id
345
branch = revision.branch
346
return branch.repository.revision_tree(revision_id)
334
348
if revision_specs is not None:
349
assert (old_revision_spec is None
350
and new_revision_spec is None)
335
351
if len(revision_specs) > 0:
336
352
old_revision_spec = revision_specs[0]
338
old_url = old_revision_spec.get_branch()
339
353
if len(revision_specs) > 1:
340
354
new_revision_spec = revision_specs[1]
342
new_url = new_revision_spec.get_branch()
345
make_paths_wt_relative = True
346
consider_relpath = True
347
if path_list is None or len(path_list) == 0:
348
# If no path is given, the current working tree is used
349
default_location = u'.'
350
consider_relpath = False
351
elif old_url is not None and new_url is not None:
352
other_paths = path_list
353
make_paths_wt_relative = False
355
default_location = path_list[0]
356
other_paths = path_list[1:]
358
def lock_tree_or_branch(wt, br):
361
add_cleanup(wt.unlock)
364
add_cleanup(br.unlock)
366
# Get the old location
369
old_url = default_location
370
working_tree, branch, relpath = \
371
controldir.ControlDir.open_containing_tree_or_branch(old_url)
372
lock_tree_or_branch(working_tree, branch)
373
if consider_relpath and relpath != '':
374
if working_tree is not None and apply_view:
375
views.check_path_in_view(working_tree, relpath)
376
specific_files.append(relpath)
377
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
380
# Get the new location
382
new_url = default_location
383
if new_url != old_url:
384
working_tree, branch, relpath = \
385
controldir.ControlDir.open_containing_tree_or_branch(new_url)
386
lock_tree_or_branch(working_tree, branch)
387
if consider_relpath and relpath != '':
388
if working_tree is not None and apply_view:
389
views.check_path_in_view(working_tree, relpath)
390
specific_files.append(relpath)
391
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
392
basis_is_default=working_tree is None)
395
# Get the specific files (all files is None, no files is [])
396
if make_paths_wt_relative and working_tree is not None:
397
other_paths = working_tree.safe_relpath_files(
399
apply_view=apply_view)
400
specific_files.extend(other_paths)
401
if len(specific_files) == 0:
402
specific_files = None
403
if (working_tree is not None and working_tree.supports_views()
405
view_files = working_tree.views.lookup_view()
407
specific_files = view_files
408
view_str = views.view_display_str(view_files)
409
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
411
# Get extra trees that ought to be searched for file-ids
413
if working_tree is not None and working_tree not in (old_tree, new_tree):
414
extra_trees = (working_tree,)
415
return (old_tree, new_tree, old_branch, new_branch,
416
specific_files, extra_trees)
419
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
420
if branch is None and tree is not None:
422
if spec is None or spec.spec is None:
425
return tree.basis_tree()
427
return branch.basis_tree()
430
return spec.as_tree(branch)
356
if old_revision_spec is None:
357
old_tree = tree.basis_tree()
359
old_tree = spec_tree(old_revision_spec)
361
if (new_revision_spec is None
362
or new_revision_spec.spec is None):
365
new_tree = spec_tree(new_revision_spec)
367
if new_tree is not tree:
368
extra_trees = (tree,)
372
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
373
external_diff_options,
374
old_label=old_label, new_label=new_label,
375
extra_trees=extra_trees)
433
378
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
434
379
external_diff_options=None,
435
380
old_label='a/', new_label='b/',
437
path_encoding='utf8',
440
context=DEFAULT_CONTEXT_AMOUNT):
441
382
"""Show in text form the changes from one tree to another.
443
:param to_file: The output stream.
444
:param specific_files: Include only changes to these files - None for all
446
:param external_diff_options: If set, use an external GNU diff and pass
448
:param extra_trees: If set, more Trees to use for looking up file ids
449
:param path_encoding: If set, the path will be encoded as specified,
450
otherwise is supposed to be utf8
451
:param format_cls: Formatter class (DiffTree subclass)
385
If set, include only changes to these files.
387
external_diff_options
388
If set, use an external GNU diff and pass these options.
391
If set, more Trees to use for looking up file ids
454
context = DEFAULT_CONTEXT_AMOUNT
455
if format_cls is None:
456
format_cls = DiffTree
457
with old_tree.lock_read():
458
395
if extra_trees is not None:
459
396
for tree in extra_trees:
461
398
new_tree.lock_read()
463
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
465
external_diff_options,
466
old_label, new_label, using,
467
context_lines=context)
468
return differ.show_diff(specific_files, extra_trees)
400
return _show_diff_trees(old_tree, new_tree, to_file,
401
specific_files, external_diff_options,
402
old_label=old_label, new_label=new_label,
403
extra_trees=extra_trees)
470
405
new_tree.unlock()
471
406
if extra_trees is not None:
472
407
for tree in extra_trees:
476
def _patch_header_date(tree, file_id, path):
477
"""Returns a timestamp suitable for use in a patch header."""
479
mtime = tree.get_file_mtime(path, file_id)
480
except FileTimestampUnavailable:
482
return timestamp.format_patch_date(mtime)
485
def get_executable_change(old_is_x, new_is_x):
486
descr = { True:"+x", False:"-x", None:"??" }
487
if old_is_x != new_is_x:
488
return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
493
class DiffPath(object):
494
"""Base type for command object that compare files"""
496
# The type or contents of the file were unsuitable for diffing
497
CANNOT_DIFF = 'CANNOT_DIFF'
498
# The file has changed in a semantic way
500
# The file content may have changed, but there is no semantic change
501
UNCHANGED = 'UNCHANGED'
503
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
506
:param old_tree: The tree to show as the old tree in the comparison
507
:param new_tree: The tree to show as new in the comparison
508
:param to_file: The file to write comparison data to
509
:param path_encoding: The character encoding to write paths in
511
self.old_tree = old_tree
512
self.new_tree = new_tree
513
self.to_file = to_file
514
self.path_encoding = path_encoding
520
def from_diff_tree(klass, diff_tree):
521
return klass(diff_tree.old_tree, diff_tree.new_tree,
522
diff_tree.to_file, diff_tree.path_encoding)
525
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
526
for file_differ in differs:
527
result = file_differ.diff(file_id, old_path, new_path, old_kind,
529
if result is not DiffPath.CANNOT_DIFF:
532
return DiffPath.CANNOT_DIFF
535
class DiffKindChange(object):
536
"""Special differ for file kind changes.
538
Represents kind change as deletion + creation. Uses the other differs
541
def __init__(self, differs):
542
self.differs = differs
548
def from_diff_tree(klass, diff_tree):
549
return klass(diff_tree.differs)
551
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
552
"""Perform comparison
554
:param file_id: The file_id of the file to compare
555
:param old_path: Path of the file in the old tree
556
:param new_path: Path of the file in the new tree
557
:param old_kind: Old file-kind of the file
558
:param new_kind: New file-kind of the file
560
if None in (old_kind, new_kind):
561
return DiffPath.CANNOT_DIFF
562
result = DiffPath._diff_many(self.differs, file_id, old_path,
563
new_path, old_kind, None)
564
if result is DiffPath.CANNOT_DIFF:
566
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
570
class DiffDirectory(DiffPath):
572
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
573
"""Perform comparison between two directories. (dummy)
576
if 'directory' not in (old_kind, new_kind):
577
return self.CANNOT_DIFF
578
if old_kind not in ('directory', None):
579
return self.CANNOT_DIFF
580
if new_kind not in ('directory', None):
581
return self.CANNOT_DIFF
585
class DiffSymlink(DiffPath):
587
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
588
"""Perform comparison between two symlinks
590
:param file_id: The file_id of the file to compare
591
:param old_path: Path of the file in the old tree
592
:param new_path: Path of the file in the new tree
593
:param old_kind: Old file-kind of the file
594
:param new_kind: New file-kind of the file
596
if 'symlink' not in (old_kind, new_kind):
597
return self.CANNOT_DIFF
598
if old_kind == 'symlink':
599
old_target = self.old_tree.get_symlink_target(old_path, file_id)
600
elif old_kind is None:
603
return self.CANNOT_DIFF
604
if new_kind == 'symlink':
605
new_target = self.new_tree.get_symlink_target(new_path, file_id)
606
elif new_kind is None:
609
return self.CANNOT_DIFF
610
return self.diff_symlink(old_target, new_target)
612
def diff_symlink(self, old_target, new_target):
613
if old_target is None:
614
self.to_file.write('=== target is %r\n' % new_target)
615
elif new_target is None:
616
self.to_file.write('=== target was %r\n' % old_target)
618
self.to_file.write('=== target changed %r => %r\n' %
619
(old_target, new_target))
623
class DiffText(DiffPath):
413
def _show_diff_trees(old_tree, new_tree, to_file,
414
specific_files, external_diff_options,
415
old_label='a/', new_label='b/', extra_trees=None):
625
417
# GNU Patch uses the epoch date to detect files that are being added
626
418
# or removed in a diff.
627
419
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
629
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
630
old_label='', new_label='', text_differ=internal_diff,
631
context_lines=DEFAULT_CONTEXT_AMOUNT):
632
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
633
self.text_differ = text_differ
634
self.old_label = old_label
635
self.new_label = new_label
636
self.path_encoding = path_encoding
637
self.context_lines = context_lines
639
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
640
"""Compare two files in unified diff format
642
:param file_id: The file_id of the file to compare
643
:param old_path: Path of the file in the old tree
644
:param new_path: Path of the file in the new tree
645
:param old_kind: Old file-kind of the file
646
:param new_kind: New file-kind of the file
648
if 'file' not in (old_kind, new_kind):
649
return self.CANNOT_DIFF
650
from_file_id = to_file_id = file_id
651
if old_kind == 'file':
652
old_date = _patch_header_date(self.old_tree, file_id, old_path)
653
elif old_kind is None:
654
old_date = self.EPOCH_DATE
657
return self.CANNOT_DIFF
658
if new_kind == 'file':
659
new_date = _patch_header_date(self.new_tree, file_id, new_path)
660
elif new_kind is None:
661
new_date = self.EPOCH_DATE
664
return self.CANNOT_DIFF
665
from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
666
to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
667
return self.diff_text(old_path, new_path, from_label, to_label,
668
from_file_id, to_file_id)
670
def diff_text(self, from_path, to_path, from_label, to_label,
671
from_file_id=None, to_file_id=None):
672
"""Diff the content of given files in two trees
674
:param from_path: The path in the from tree. If None,
675
the file is not present in the from tree.
676
:param to_path: The path in the to tree. This may refer
677
to a different file from from_path. If None,
678
the file is not present in the to tree.
679
:param from_file_id: The id of the file in the from tree or None if
681
:param to_file_id: The id of the file in the to tree or None if
684
def _get_text(tree, file_id, path):
687
return tree.get_file_lines(path, file_id)
689
from_text = _get_text(self.old_tree, from_file_id, from_path)
690
to_text = _get_text(self.new_tree, to_file_id, to_path)
691
self.text_differ(from_label, from_text, to_label, to_text,
692
self.to_file, path_encoding=self.path_encoding,
693
context_lines=self.context_lines)
694
except errors.BinaryFile:
696
("Binary files %s and %s differ\n" %
697
(from_label, to_label)).encode(self.path_encoding, 'replace'))
701
class DiffFromTool(DiffPath):
703
def __init__(self, command_template, old_tree, new_tree, to_file,
704
path_encoding='utf-8'):
705
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
706
self.command_template = command_template
707
self._root = osutils.mkdtemp(prefix='brz-diff-')
710
def from_string(klass, command_string, old_tree, new_tree, to_file,
711
path_encoding='utf-8'):
712
command_template = cmdline.split(command_string)
713
if '@' not in command_string:
714
command_template.extend(['@old_path', '@new_path'])
715
return klass(command_template, old_tree, new_tree, to_file,
719
def make_from_diff_tree(klass, command_string, external_diff_options=None):
720
def from_diff_tree(diff_tree):
721
full_command_string = [command_string]
722
if external_diff_options is not None:
723
full_command_string += ' ' + external_diff_options
724
return klass.from_string(full_command_string, diff_tree.old_tree,
725
diff_tree.new_tree, diff_tree.to_file)
726
return from_diff_tree
728
def _get_command(self, old_path, new_path):
729
my_map = {'old_path': old_path, 'new_path': new_path}
730
command = [AtTemplate(t).substitute(my_map) for t in
731
self.command_template]
732
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
735
if isinstance(c, text_type):
736
command_encoded.append(c.encode('mbcs'))
738
command_encoded.append(c)
739
return command_encoded
743
def _execute(self, old_path, new_path):
744
command = self._get_command(old_path, new_path)
746
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
749
if e.errno == errno.ENOENT:
750
raise errors.ExecutableMissing(command[0])
753
self.to_file.write(proc.stdout.read())
756
def _try_symlink_root(self, tree, prefix):
757
if (getattr(tree, 'abspath', None) is None
758
or not osutils.host_os_dereferences_symlinks()):
761
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
763
if e.errno != errno.EEXIST:
769
"""Returns safe encoding for passing file path to diff tool"""
770
if sys.platform == 'win32':
773
# Don't fallback to 'utf-8' because subprocess may not be able to
774
# handle utf-8 correctly when locale is not utf-8.
775
return sys.getfilesystemencoding() or 'ascii'
777
def _is_safepath(self, path):
778
"""Return true if `path` may be able to pass to subprocess."""
781
return path == path.encode(fenc).decode(fenc)
785
def _safe_filename(self, prefix, relpath):
786
"""Replace unsafe character in `relpath` then join `self._root`,
787
`prefix` and `relpath`."""
789
# encoded_str.replace('?', '_') may break multibyte char.
790
# So we should encode, decode, then replace(u'?', u'_')
791
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
792
relpath_tmp = relpath_tmp.replace(u'?', u'_')
793
return osutils.pathjoin(self._root, prefix, relpath_tmp)
795
def _write_file(self, relpath, tree, prefix, force_temp=False,
796
allow_write=False, file_id=None):
797
if not force_temp and isinstance(tree, WorkingTree):
798
full_path = tree.abspath(relpath)
799
if self._is_safepath(full_path):
802
full_path = self._safe_filename(prefix, relpath)
803
if not force_temp and 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(relpath, file_id)
813
with open(full_path, 'wb') as target:
814
osutils.pumpfile(source, target)
818
mtime = tree.get_file_mtime(relpath, file_id)
819
except FileTimestampUnavailable:
822
os.utime(full_path, (mtime, mtime))
824
osutils.make_readonly(full_path)
827
def _prepare_files(self, old_path, new_path, force_temp=False,
828
allow_write_new=False, file_id=None):
829
old_disk_path = self._write_file(old_path, self.old_tree, 'old',
830
force_temp, file_id=file_id)
831
new_disk_path = self._write_file(new_path, self.new_tree, 'new',
832
force_temp, file_id=file_id,
833
allow_write=allow_write_new)
834
return old_disk_path, new_disk_path
838
osutils.rmtree(self._root)
840
if e.errno != errno.ENOENT:
841
mutter("The temporary directory \"%s\" was not "
842
"cleanly removed: %s." % (self._root, e))
844
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
845
if (old_kind, new_kind) != ('file', 'file'):
846
return DiffPath.CANNOT_DIFF
847
(old_disk_path, new_disk_path) = self._prepare_files(
848
old_path, new_path, file_id=file_id)
849
self._execute(old_disk_path, new_disk_path)
851
def edit_file(self, old_path, new_path, file_id=None):
852
"""Use this tool to edit a file.
854
A temporary copy will be edited, and the new contents will be
857
:param file_id: The id of the file to edit.
858
:return: The new contents of the file.
860
old_abs_path, new_abs_path = self._prepare_files(
861
old_path, new_path, allow_write_new=True, force_temp=True,
863
command = self._get_command(old_abs_path, new_abs_path)
864
subprocess.call(command, cwd=self._root)
865
with open(new_abs_path, 'rb') as new_file:
866
return new_file.read()
869
class DiffTree(object):
870
"""Provides textual representations of the difference between two trees.
872
A DiffTree examines two trees and where a file-id has altered
873
between them, generates a textual representation of the difference.
874
DiffTree uses a sequence of DiffPath objects which are each
875
given the opportunity to handle a given altered fileid. The list
876
of DiffPath objects can be extended globally by appending to
877
DiffTree.diff_factories, or for a specific diff operation by
878
supplying the extra_factories option to the appropriate method.
421
# TODO: Generation of pseudo-diffs for added/deleted files could
422
# be usefully made into a much faster special case.
424
if external_diff_options:
425
assert isinstance(external_diff_options, basestring)
426
opts = external_diff_options.split()
427
def diff_file(olab, olines, nlab, nlines, to_file):
428
external_diff(olab, olines, nlab, nlines, to_file, opts)
430
diff_file = internal_diff
432
delta = new_tree.changes_from(old_tree,
433
specific_files=specific_files,
434
extra_trees=extra_trees, require_versioned=True)
437
for path, file_id, kind in delta.removed:
439
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
440
old_name = '%s%s\t%s' % (old_label, path,
441
_patch_header_date(old_tree, file_id, path))
442
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
443
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
444
new_name, None, None, to_file)
445
for path, file_id, kind in delta.added:
447
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
448
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
449
new_name = '%s%s\t%s' % (new_label, path,
450
_patch_header_date(new_tree, file_id, path))
451
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
452
old_name, None, None, to_file,
454
for (old_path, new_path, file_id, kind,
455
text_modified, meta_modified) in delta.renamed:
457
prop_str = get_prop_change(meta_modified)
458
print >>to_file, '=== renamed %s %r => %r%s' % (
459
kind, old_path.encode('utf8'),
460
new_path.encode('utf8'), prop_str)
461
old_name = '%s%s\t%s' % (old_label, old_path,
462
_patch_header_date(old_tree, file_id,
464
new_name = '%s%s\t%s' % (new_label, new_path,
465
_patch_header_date(new_tree, file_id,
467
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
469
text_modified, kind, to_file, diff_file)
470
for path, file_id, kind, text_modified, meta_modified in delta.modified:
472
prop_str = get_prop_change(meta_modified)
473
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
474
old_name = '%s%s\t%s' % (old_label, path,
475
_patch_header_date(old_tree, file_id, path))
476
new_name = '%s%s\t%s' % (new_label, path,
477
_patch_header_date(new_tree, file_id, path))
479
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
481
True, kind, to_file, diff_file)
486
def _patch_header_date(tree, file_id, path):
487
"""Returns a timestamp suitable for use in a patch header."""
488
return timestamp.format_patch_date(tree.get_file_mtime(file_id, path))
491
def _raise_if_nonexistent(paths, old_tree, new_tree):
492
"""Complain if paths are not in either inventory or tree.
494
It's OK with the files exist in either tree's inventory, or
495
if they exist in the tree but are not versioned.
497
This can be used by operations such as bzr status that can accept
498
unknown or ignored files.
881
# list of factories that can provide instances of DiffPath objects
882
# may be extended by plugins.
883
diff_factories = [DiffSymlink.from_diff_tree,
884
DiffDirectory.from_diff_tree]
886
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
887
diff_text=None, extra_factories=None):
890
:param old_tree: Tree to show as old in the comparison
891
:param new_tree: Tree to show as new in the comparison
892
:param to_file: File to write comparision to
893
:param path_encoding: Character encoding to write paths in
894
:param diff_text: DiffPath-type object to use as a last resort for
896
:param extra_factories: Factories of DiffPaths to try before any other
898
if diff_text is None:
899
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
900
'', '', internal_diff)
901
self.old_tree = old_tree
902
self.new_tree = new_tree
903
self.to_file = to_file
904
self.path_encoding = path_encoding
906
if extra_factories is not None:
907
self.differs.extend(f(self) for f in extra_factories)
908
self.differs.extend(f(self) for f in self.diff_factories)
909
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
912
def from_trees_options(klass, old_tree, new_tree, to_file,
913
path_encoding, external_diff_options, old_label,
914
new_label, using, context_lines):
915
"""Factory for producing a DiffTree.
917
Designed to accept options used by show_diff_trees.
919
:param old_tree: The tree to show as old in the comparison
920
:param new_tree: The tree to show as new in the comparison
921
:param to_file: File to write comparisons to
922
:param path_encoding: Character encoding to use for writing paths
923
:param external_diff_options: If supplied, use the installed diff
924
binary to perform file comparison, using supplied options.
925
:param old_label: Prefix to use for old file labels
926
:param new_label: Prefix to use for new file labels
927
:param using: Commandline to use to invoke an external diff tool
929
if using is not None:
930
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
933
if external_diff_options:
934
opts = external_diff_options.split()
935
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
936
""":param path_encoding: not used but required
937
to match the signature of internal_diff.
939
external_diff(olab, olines, nlab, nlines, to_file, opts)
941
diff_file = internal_diff
942
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
943
old_label, new_label, diff_file, context_lines=context_lines)
944
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
947
def show_diff(self, specific_files, extra_trees=None):
948
"""Write tree diff to self.to_file
950
:param specific_files: the specific files to compare (recursive)
951
:param extra_trees: extra trees to use for mapping paths to file_ids
954
return self._show_diff(specific_files, extra_trees)
956
for differ in self.differs:
959
def _show_diff(self, specific_files, extra_trees):
960
# TODO: Generation of pseudo-diffs for added/deleted files could
961
# be usefully made into a much faster special case.
962
iterator = self.new_tree.iter_changes(self.old_tree,
963
specific_files=specific_files,
964
extra_trees=extra_trees,
965
require_versioned=True)
967
def changes_key(change):
968
old_path, new_path = change[1]
973
def get_encoded_path(path):
975
return path.encode(self.path_encoding, "replace")
976
for (file_id, paths, changed_content, versioned, parent, name, kind,
977
executable) in sorted(iterator, key=changes_key):
978
# The root does not get diffed, and items with no known kind (that
979
# is, missing) in both trees are skipped as well.
980
if parent == (None, None) or kind == (None, None):
982
oldpath, newpath = paths
983
oldpath_encoded = get_encoded_path(paths[0])
984
newpath_encoded = get_encoded_path(paths[1])
985
old_present = (kind[0] is not None and versioned[0])
986
new_present = (kind[1] is not None and versioned[1])
987
renamed = (parent[0], name[0]) != (parent[1], name[1])
989
properties_changed = []
990
properties_changed.extend(get_executable_change(executable[0], executable[1]))
992
if properties_changed:
993
prop_str = b" (properties changed: %s)" % (
994
b", ".join(properties_changed),)
998
if (old_present, new_present) == (True, False):
999
self.to_file.write(b"=== removed %s '%s'\n" %
1000
(kind[0].encode('ascii'), oldpath_encoded))
1002
elif (old_present, new_present) == (False, True):
1003
self.to_file.write(b"=== added %s '%s'\n" %
1004
(kind[1].encode('ascii'), newpath_encoded))
1007
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1008
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1010
# if it was produced by iter_changes, it must be
1011
# modified *somehow*, either content or execute bit.
1012
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1013
newpath_encoded, prop_str))
1015
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
1021
def diff(self, file_id, old_path, new_path):
1022
"""Perform a diff of a single file
1024
:param file_id: file-id of the file
1025
:param old_path: The path of the file in the old tree
1026
:param new_path: The path of the file in the new tree
1028
if old_path is None:
1031
old_kind = self.old_tree.kind(old_path, file_id)
1032
if new_path is None:
1035
new_kind = self.new_tree.kind(new_path, file_id)
1036
self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1038
def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
1039
result = DiffPath._diff_many(self.differs, file_id, old_path,
1040
new_path, old_kind, new_kind)
1041
if result is DiffPath.CANNOT_DIFF:
1042
error_path = new_path
1043
if error_path is None:
1044
error_path = old_path
1045
raise errors.NoDiffFound(error_path)
1048
format_registry = Registry()
1049
format_registry.register('default', DiffTree)
500
mutter("check paths: %r", paths)
503
s = old_tree.filter_unversioned_files(paths)
504
s = new_tree.filter_unversioned_files(s)
505
s = [path for path in s if not new_tree.has_filename(path)]
507
raise errors.PathsDoNotExist(sorted(s))
510
def get_prop_change(meta_modified):
512
return " (properties changed)"
517
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
518
new_path, new_tree, text_modified,
519
kind, to_file, diff_file):
521
new_entry = new_tree.inventory[file_id]
522
old_tree.inventory[file_id].diff(diff_file,