275
225
msg = 'signal %d' % (-rc)
277
227
msg = 'exit code %d' % rc
279
raise errors.BzrError('external diff failed with %s; command: %r'
229
raise BzrError('external diff failed with %s; command: %r'
284
234
oldtmpf.close() # and delete
288
# Warn in case the file couldn't be deleted (in case windows still
289
# holds the file open, but not if the files have already been
294
if e.errno not in (errno.ENOENT,):
295
warning('Failed to delete temporary file: %s %s', path, e)
301
def get_trees_and_branches_to_diff_locked(
302
path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
303
"""Get the trees and specific files to diff given a list of paths.
305
This method works out the trees to be diff'ed and the files of
306
interest within those trees.
309
the list of arguments passed to the diff command
310
:param revision_specs:
311
Zero, one or two RevisionSpecs from the diff command line,
312
saying what revisions to compare.
314
The url of the old branch or tree. If None, the tree to use is
315
taken from the first path, if any, or the current working tree.
317
The url of the new branch or tree. If None, the tree to use is
318
taken from the first path, if any, or the current working tree.
320
a callable like Command.add_cleanup. get_trees_and_branches_to_diff
321
will register cleanups that must be run to unlock the trees, etc.
323
if True and a view is set, apply the view or check that the paths
326
a tuple of (old_tree, new_tree, old_branch, new_branch,
327
specific_files, extra_trees) where extra_trees is a sequence of
328
additional trees to search in for file-ids. The trees and branches
329
will be read-locked until the cleanups registered via the add_cleanup
332
# Get the old and new revision specs
333
old_revision_spec = None
334
new_revision_spec = None
335
if revision_specs is not None:
336
if len(revision_specs) > 0:
337
old_revision_spec = revision_specs[0]
339
old_url = old_revision_spec.get_branch()
340
if len(revision_specs) > 1:
341
new_revision_spec = revision_specs[1]
343
new_url = new_revision_spec.get_branch()
346
make_paths_wt_relative = True
347
consider_relpath = True
348
if path_list is None or len(path_list) == 0:
349
# If no path is given, the current working tree is used
350
default_location = u'.'
351
consider_relpath = False
352
elif old_url is not None and new_url is not None:
353
other_paths = path_list
354
make_paths_wt_relative = False
356
default_location = path_list[0]
357
other_paths = path_list[1:]
359
def lock_tree_or_branch(wt, br):
362
add_cleanup(wt.unlock)
365
add_cleanup(br.unlock)
367
# Get the old location
370
old_url = default_location
371
working_tree, branch, relpath = \
372
controldir.ControlDir.open_containing_tree_or_branch(old_url)
373
lock_tree_or_branch(working_tree, branch)
374
if consider_relpath and relpath != '':
375
if working_tree is not None and apply_view:
376
views.check_path_in_view(working_tree, relpath)
377
specific_files.append(relpath)
378
old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
381
# Get the new location
383
new_url = default_location
384
if new_url != old_url:
385
working_tree, branch, relpath = \
386
controldir.ControlDir.open_containing_tree_or_branch(new_url)
387
lock_tree_or_branch(working_tree, branch)
388
if consider_relpath and relpath != '':
389
if working_tree is not None and apply_view:
390
views.check_path_in_view(working_tree, relpath)
391
specific_files.append(relpath)
392
new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
393
basis_is_default=working_tree is None)
396
# Get the specific files (all files is None, no files is [])
397
if make_paths_wt_relative and working_tree is not None:
398
other_paths = working_tree.safe_relpath_files(
400
apply_view=apply_view)
401
specific_files.extend(other_paths)
402
if len(specific_files) == 0:
403
specific_files = None
404
if (working_tree is not None and working_tree.supports_views()
406
view_files = working_tree.views.lookup_view()
408
specific_files = view_files
409
view_str = views.view_display_str(view_files)
410
note(gettext("*** Ignoring files outside view. View is %s") % view_str)
412
# Get extra trees that ought to be searched for file-ids
414
if working_tree is not None and working_tree not in (old_tree, new_tree):
415
extra_trees = (working_tree,)
416
return (old_tree, new_tree, old_branch, new_branch,
417
specific_files, extra_trees)
420
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
421
if branch is None and tree is not None:
423
if spec is None or spec.spec is None:
426
return tree.basis_tree()
428
return branch.basis_tree()
431
return spec.as_tree(branch)
236
# Clean up. Warn in case the files couldn't be deleted
237
# (in case windows still holds the file open, but not
238
# if the files have already been deleted)
240
os.remove(old_abspath)
242
if e.errno not in (errno.ENOENT,):
243
warning('Failed to delete temporary file: %s %s',
246
os.remove(new_abspath)
248
if e.errno not in (errno.ENOENT,):
249
warning('Failed to delete temporary file: %s %s',
253
@deprecated_function(zero_eight)
254
def show_diff(b, from_spec, specific_files, external_diff_options=None,
255
revision2=None, output=None, b2=None):
256
"""Shortcut for showing the diff to the working tree.
258
Please use show_diff_trees instead.
264
None for 'basis tree', or otherwise the old revision to compare against.
266
The more general form is show_diff_trees(), where the caller
267
supplies any two trees.
272
if from_spec is None:
273
old_tree = b.bzrdir.open_workingtree()
275
old_tree = old_tree = old_tree.basis_tree()
277
old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
279
if revision2 is None:
281
new_tree = b.bzrdir.open_workingtree()
283
new_tree = b2.bzrdir.open_workingtree()
285
new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
287
return show_diff_trees(old_tree, new_tree, output, specific_files,
288
external_diff_options)
291
def diff_cmd_helper(tree, specific_files, external_diff_options,
292
old_revision_spec=None, new_revision_spec=None,
293
old_label='a/', new_label='b/'):
294
"""Helper for cmd_diff.
300
The specific files to compare, or None
302
external_diff_options
303
If non-None, run an external diff, and pass it these options
306
If None, use basis tree as old revision, otherwise use the tree for
307
the specified revision.
310
If None, use working tree as new revision, otherwise use the tree for
311
the specified revision.
313
The more general form is show_diff_trees(), where the caller
314
supplies any two trees.
318
revision = spec.in_store(tree.branch)
320
revision = spec.in_store(None)
321
revision_id = revision.rev_id
322
branch = revision.branch
323
return branch.repository.revision_tree(revision_id)
324
if old_revision_spec is None:
325
old_tree = tree.basis_tree()
327
old_tree = spec_tree(old_revision_spec)
329
if new_revision_spec is None:
332
new_tree = spec_tree(new_revision_spec)
333
if new_tree is not tree:
334
extra_trees = (tree,)
338
return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
339
external_diff_options,
340
old_label=old_label, new_label=new_label,
341
extra_trees=extra_trees)
434
344
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
435
345
external_diff_options=None,
436
346
old_label='a/', new_label='b/',
438
path_encoding='utf8',
441
context=DEFAULT_CONTEXT_AMOUNT):
442
348
"""Show in text form the changes from one tree to another.
444
:param to_file: The output stream.
445
:param specific_files: Include only changes to these files - None for all
447
:param external_diff_options: If set, use an external GNU diff and pass
449
:param extra_trees: If set, more Trees to use for looking up file ids
450
:param path_encoding: If set, the path will be encoded as specified,
451
otherwise is supposed to be utf8
452
:param format_cls: Formatter class (DiffTree subclass)
351
If set, include only changes to these files.
353
external_diff_options
354
If set, use an external GNU diff and pass these options.
357
If set, more Trees to use for looking up file ids
455
context = DEFAULT_CONTEXT_AMOUNT
456
if format_cls is None:
457
format_cls = DiffTree
458
with old_tree.lock_read():
459
if extra_trees is not None:
460
for tree in extra_trees:
462
361
new_tree.lock_read()
464
differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
466
external_diff_options,
467
old_label, new_label, using,
468
context_lines=context)
469
return differ.show_diff(specific_files, extra_trees)
363
return _show_diff_trees(old_tree, new_tree, to_file,
364
specific_files, external_diff_options,
365
old_label=old_label, new_label=new_label,
366
extra_trees=extra_trees)
471
368
new_tree.unlock()
472
if extra_trees is not None:
473
for tree in extra_trees:
477
def _patch_header_date(tree, file_id, path):
478
"""Returns a timestamp suitable for use in a patch header."""
480
mtime = tree.get_file_mtime(path, file_id)
481
except FileTimestampUnavailable:
483
return timestamp.format_patch_date(mtime)
486
def get_executable_change(old_is_x, new_is_x):
487
descr = { True:b"+x", False:b"-x", None:b"??" }
488
if old_is_x != new_is_x:
489
return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
494
class DiffPath(object):
495
"""Base type for command object that compare files"""
497
# The type or contents of the file were unsuitable for diffing
498
CANNOT_DIFF = 'CANNOT_DIFF'
499
# The file has changed in a semantic way
501
# The file content may have changed, but there is no semantic change
502
UNCHANGED = 'UNCHANGED'
504
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
507
:param old_tree: The tree to show as the old tree in the comparison
508
:param new_tree: The tree to show as new in the comparison
509
:param to_file: The file to write comparison data to
510
:param path_encoding: The character encoding to write paths in
512
self.old_tree = old_tree
513
self.new_tree = new_tree
514
self.to_file = to_file
515
self.path_encoding = path_encoding
521
def from_diff_tree(klass, diff_tree):
522
return klass(diff_tree.old_tree, diff_tree.new_tree,
523
diff_tree.to_file, diff_tree.path_encoding)
526
def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
527
for file_differ in differs:
528
result = file_differ.diff(file_id, old_path, new_path, old_kind,
530
if result is not DiffPath.CANNOT_DIFF:
533
return DiffPath.CANNOT_DIFF
536
class DiffKindChange(object):
537
"""Special differ for file kind changes.
539
Represents kind change as deletion + creation. Uses the other differs
542
def __init__(self, differs):
543
self.differs = differs
549
def from_diff_tree(klass, diff_tree):
550
return klass(diff_tree.differs)
552
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
553
"""Perform comparison
555
:param file_id: The file_id of the file to compare
556
:param old_path: Path of the file in the old tree
557
:param new_path: Path of the file in the new tree
558
:param old_kind: Old file-kind of the file
559
:param new_kind: New file-kind of the file
561
if None in (old_kind, new_kind):
562
return DiffPath.CANNOT_DIFF
563
result = DiffPath._diff_many(self.differs, file_id, old_path,
564
new_path, old_kind, None)
565
if result is DiffPath.CANNOT_DIFF:
567
return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
571
class DiffDirectory(DiffPath):
573
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
574
"""Perform comparison between two directories. (dummy)
577
if 'directory' not in (old_kind, new_kind):
578
return self.CANNOT_DIFF
579
if old_kind not in ('directory', None):
580
return self.CANNOT_DIFF
581
if new_kind not in ('directory', None):
582
return self.CANNOT_DIFF
586
class DiffSymlink(DiffPath):
588
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
589
"""Perform comparison between two symlinks
591
:param file_id: The file_id of the file to compare
592
:param old_path: Path of the file in the old tree
593
:param new_path: Path of the file in the new tree
594
:param old_kind: Old file-kind of the file
595
:param new_kind: New file-kind of the file
597
if 'symlink' not in (old_kind, new_kind):
598
return self.CANNOT_DIFF
599
if old_kind == 'symlink':
600
old_target = self.old_tree.get_symlink_target(old_path, file_id)
601
elif old_kind is None:
604
return self.CANNOT_DIFF
605
if new_kind == 'symlink':
606
new_target = self.new_tree.get_symlink_target(new_path, file_id)
607
elif new_kind is None:
610
return self.CANNOT_DIFF
611
return self.diff_symlink(old_target, new_target)
613
def diff_symlink(self, old_target, new_target):
614
if old_target is None:
615
self.to_file.write(b'=== target is \'%s\'\n' %
616
new_target.encode(self.path_encoding, 'replace'))
617
elif new_target is None:
618
self.to_file.write(b'=== target was \'%s\'\n' %
619
old_target.encode(self.path_encoding, 'replace'))
621
self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
622
(old_target.encode(self.path_encoding, 'replace'),
623
new_target.encode(self.path_encoding, 'replace')))
627
class DiffText(DiffPath):
373
def _show_diff_trees(old_tree, new_tree, to_file,
374
specific_files, external_diff_options,
375
old_label='a/', new_label='b/', extra_trees=None):
629
377
# GNU Patch uses the epoch date to detect files that are being added
630
378
# or removed in a diff.
631
379
EPOCH_DATE = '1970-01-01 00:00:00 +0000'
633
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
634
old_label='', new_label='', text_differ=internal_diff,
635
context_lines=DEFAULT_CONTEXT_AMOUNT):
636
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
637
self.text_differ = text_differ
638
self.old_label = old_label
639
self.new_label = new_label
640
self.path_encoding = path_encoding
641
self.context_lines = context_lines
643
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
644
"""Compare two files in unified diff format
646
:param file_id: The file_id of the file to compare
647
:param old_path: Path of the file in the old tree
648
:param new_path: Path of the file in the new tree
649
:param old_kind: Old file-kind of the file
650
:param new_kind: New file-kind of the file
652
if 'file' not in (old_kind, new_kind):
653
return self.CANNOT_DIFF
654
from_file_id = to_file_id = file_id
655
if old_kind == 'file':
656
old_date = _patch_header_date(self.old_tree, file_id, old_path)
657
elif old_kind is None:
658
old_date = self.EPOCH_DATE
661
return self.CANNOT_DIFF
662
if new_kind == 'file':
663
new_date = _patch_header_date(self.new_tree, file_id, new_path)
664
elif new_kind is None:
665
new_date = self.EPOCH_DATE
668
return self.CANNOT_DIFF
669
from_label = '%s%s\t%s' % (self.old_label, old_path,
671
to_label = '%s%s\t%s' % (self.new_label, new_path,
673
return self.diff_text(old_path, new_path, from_label, to_label,
674
from_file_id, to_file_id)
676
def diff_text(self, from_path, to_path, from_label, to_label,
677
from_file_id=None, to_file_id=None):
678
"""Diff the content of given files in two trees
680
:param from_path: The path in the from tree. If None,
681
the file is not present in the from tree.
682
:param to_path: The path in the to tree. This may refer
683
to a different file from from_path. If None,
684
the file is not present in the to tree.
685
:param from_file_id: The id of the file in the from tree or None if
687
:param to_file_id: The id of the file in the to tree or None if
690
def _get_text(tree, file_id, path):
693
return tree.get_file_lines(path, file_id)
695
from_text = _get_text(self.old_tree, from_file_id, from_path)
696
to_text = _get_text(self.new_tree, to_file_id, to_path)
697
self.text_differ(from_label, from_text, to_label, to_text,
698
self.to_file, path_encoding=self.path_encoding,
699
context_lines=self.context_lines)
700
except errors.BinaryFile:
702
("Binary files %s and %s differ\n" %
703
(from_label, to_label)).encode(self.path_encoding, 'replace'))
707
class DiffFromTool(DiffPath):
709
def __init__(self, command_template, old_tree, new_tree, to_file,
710
path_encoding='utf-8'):
711
DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
712
self.command_template = command_template
713
self._root = osutils.mkdtemp(prefix='brz-diff-')
716
def from_string(klass, command_string, old_tree, new_tree, to_file,
717
path_encoding='utf-8'):
718
command_template = cmdline.split(command_string)
719
if '@' not in command_string:
720
command_template.extend(['@old_path', '@new_path'])
721
return klass(command_template, old_tree, new_tree, to_file,
725
def make_from_diff_tree(klass, command_string, external_diff_options=None):
726
def from_diff_tree(diff_tree):
727
full_command_string = [command_string]
728
if external_diff_options is not None:
729
full_command_string += ' ' + external_diff_options
730
return klass.from_string(full_command_string, diff_tree.old_tree,
731
diff_tree.new_tree, diff_tree.to_file)
732
return from_diff_tree
734
def _get_command(self, old_path, new_path):
735
my_map = {'old_path': old_path, 'new_path': new_path}
736
command = [AtTemplate(t).substitute(my_map) for t in
737
self.command_template]
738
if sys.platform == 'win32': # Popen doesn't accept unicode on win32
741
if isinstance(c, text_type):
742
command_encoded.append(c.encode('mbcs'))
744
command_encoded.append(c)
745
return command_encoded
749
def _execute(self, old_path, new_path):
750
command = self._get_command(old_path, new_path)
752
proc = subprocess.Popen(command, stdout=subprocess.PIPE,
755
if e.errno == errno.ENOENT:
756
raise errors.ExecutableMissing(command[0])
759
self.to_file.write(proc.stdout.read())
763
def _try_symlink_root(self, tree, prefix):
764
if (getattr(tree, 'abspath', None) is None
765
or not osutils.host_os_dereferences_symlinks()):
768
os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
770
if e.errno != errno.EEXIST:
776
"""Returns safe encoding for passing file path to diff tool"""
777
if sys.platform == 'win32':
780
# Don't fallback to 'utf-8' because subprocess may not be able to
781
# handle utf-8 correctly when locale is not utf-8.
782
return sys.getfilesystemencoding() or 'ascii'
784
def _is_safepath(self, path):
785
"""Return true if `path` may be able to pass to subprocess."""
788
return path == path.encode(fenc).decode(fenc)
792
def _safe_filename(self, prefix, relpath):
793
"""Replace unsafe character in `relpath` then join `self._root`,
794
`prefix` and `relpath`."""
796
# encoded_str.replace('?', '_') may break multibyte char.
797
# So we should encode, decode, then replace(u'?', u'_')
798
relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
799
relpath_tmp = relpath_tmp.replace(u'?', u'_')
800
return osutils.pathjoin(self._root, prefix, relpath_tmp)
802
def _write_file(self, relpath, tree, prefix, force_temp=False,
803
allow_write=False, file_id=None):
804
if not force_temp and isinstance(tree, WorkingTree):
805
full_path = tree.abspath(relpath)
806
if self._is_safepath(full_path):
809
full_path = self._safe_filename(prefix, relpath)
810
if not force_temp and self._try_symlink_root(tree, prefix):
812
parent_dir = osutils.dirname(full_path)
814
os.makedirs(parent_dir)
816
if e.errno != errno.EEXIST:
818
source = tree.get_file(relpath, file_id)
820
with open(full_path, 'wb') as target:
821
osutils.pumpfile(source, target)
825
mtime = tree.get_file_mtime(relpath, file_id)
826
except FileTimestampUnavailable:
829
os.utime(full_path, (mtime, mtime))
831
osutils.make_readonly(full_path)
834
def _prepare_files(self, old_path, new_path, force_temp=False,
835
allow_write_new=False, file_id=None):
836
old_disk_path = self._write_file(old_path, self.old_tree, 'old',
837
force_temp, file_id=file_id)
838
new_disk_path = self._write_file(new_path, self.new_tree, 'new',
839
force_temp, file_id=file_id,
840
allow_write=allow_write_new)
841
return old_disk_path, new_disk_path
845
osutils.rmtree(self._root)
847
if e.errno != errno.ENOENT:
848
mutter("The temporary directory \"%s\" was not "
849
"cleanly removed: %s." % (self._root, e))
851
def diff(self, file_id, old_path, new_path, old_kind, new_kind):
852
if (old_kind, new_kind) != ('file', 'file'):
853
return DiffPath.CANNOT_DIFF
854
(old_disk_path, new_disk_path) = self._prepare_files(
855
old_path, new_path, file_id=file_id)
856
self._execute(old_disk_path, new_disk_path)
858
def edit_file(self, old_path, new_path, file_id=None):
859
"""Use this tool to edit a file.
861
A temporary copy will be edited, and the new contents will be
864
:param file_id: The id of the file to edit.
865
:return: The new contents of the file.
867
old_abs_path, new_abs_path = self._prepare_files(
868
old_path, new_path, allow_write_new=True, force_temp=True,
870
command = self._get_command(old_abs_path, new_abs_path)
871
subprocess.call(command, cwd=self._root)
872
with open(new_abs_path, 'rb') as new_file:
873
return new_file.read()
876
class DiffTree(object):
877
"""Provides textual representations of the difference between two trees.
879
A DiffTree examines two trees and where a file-id has altered
880
between them, generates a textual representation of the difference.
881
DiffTree uses a sequence of DiffPath objects which are each
882
given the opportunity to handle a given altered fileid. The list
883
of DiffPath objects can be extended globally by appending to
884
DiffTree.diff_factories, or for a specific diff operation by
885
supplying the extra_factories option to the appropriate method.
381
# TODO: Generation of pseudo-diffs for added/deleted files could
382
# be usefully made into a much faster special case.
384
if external_diff_options:
385
assert isinstance(external_diff_options, basestring)
386
opts = external_diff_options.split()
387
def diff_file(olab, olines, nlab, nlines, to_file):
388
external_diff(olab, olines, nlab, nlines, to_file, opts)
390
diff_file = internal_diff
392
delta = new_tree.changes_from(old_tree,
393
specific_files=specific_files,
394
extra_trees=extra_trees, require_versioned=True)
397
for path, file_id, kind in delta.removed:
399
print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
400
old_name = '%s%s\t%s' % (old_label, path,
401
_patch_header_date(old_tree, file_id, path))
402
new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
403
old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
404
new_name, None, None, to_file)
405
for path, file_id, kind in delta.added:
407
print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
408
old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
409
new_name = '%s%s\t%s' % (new_label, path,
410
_patch_header_date(new_tree, file_id, path))
411
new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
412
old_name, None, None, to_file,
414
for (old_path, new_path, file_id, kind,
415
text_modified, meta_modified) in delta.renamed:
417
prop_str = get_prop_change(meta_modified)
418
print >>to_file, '=== renamed %s %r => %r%s' % (
419
kind, old_path.encode('utf8'),
420
new_path.encode('utf8'), prop_str)
421
old_name = '%s%s\t%s' % (old_label, old_path,
422
_patch_header_date(old_tree, file_id,
424
new_name = '%s%s\t%s' % (new_label, new_path,
425
_patch_header_date(new_tree, file_id,
427
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
429
text_modified, kind, to_file, diff_file)
430
for path, file_id, kind, text_modified, meta_modified in delta.modified:
432
prop_str = get_prop_change(meta_modified)
433
print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
434
old_name = '%s%s\t%s' % (old_label, path,
435
_patch_header_date(old_tree, file_id, path))
436
new_name = '%s%s\t%s' % (new_label, path,
437
_patch_header_date(new_tree, file_id, path))
439
_maybe_diff_file_or_symlink(old_name, old_tree, file_id,
441
True, kind, to_file, diff_file)
446
def _patch_header_date(tree, file_id, path):
447
"""Returns a timestamp suitable for use in a patch header."""
448
tm = time.gmtime(tree.get_file_mtime(file_id, path))
449
return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
452
def _raise_if_nonexistent(paths, old_tree, new_tree):
453
"""Complain if paths are not in either inventory or tree.
455
It's OK with the files exist in either tree's inventory, or
456
if they exist in the tree but are not versioned.
458
This can be used by operations such as bzr status that can accept
459
unknown or ignored files.
888
# list of factories that can provide instances of DiffPath objects
889
# may be extended by plugins.
890
diff_factories = [DiffSymlink.from_diff_tree,
891
DiffDirectory.from_diff_tree]
893
def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
894
diff_text=None, extra_factories=None):
897
:param old_tree: Tree to show as old in the comparison
898
:param new_tree: Tree to show as new in the comparison
899
:param to_file: File to write comparision to
900
:param path_encoding: Character encoding to write paths in
901
:param diff_text: DiffPath-type object to use as a last resort for
903
:param extra_factories: Factories of DiffPaths to try before any other
905
if diff_text is None:
906
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
907
'', '', internal_diff)
908
self.old_tree = old_tree
909
self.new_tree = new_tree
910
self.to_file = to_file
911
self.path_encoding = path_encoding
913
if extra_factories is not None:
914
self.differs.extend(f(self) for f in extra_factories)
915
self.differs.extend(f(self) for f in self.diff_factories)
916
self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
919
def from_trees_options(klass, old_tree, new_tree, to_file,
920
path_encoding, external_diff_options, old_label,
921
new_label, using, context_lines):
922
"""Factory for producing a DiffTree.
924
Designed to accept options used by show_diff_trees.
926
:param old_tree: The tree to show as old in the comparison
927
:param new_tree: The tree to show as new in the comparison
928
:param to_file: File to write comparisons to
929
:param path_encoding: Character encoding to use for writing paths
930
:param external_diff_options: If supplied, use the installed diff
931
binary to perform file comparison, using supplied options.
932
:param old_label: Prefix to use for old file labels
933
:param new_label: Prefix to use for new file labels
934
:param using: Commandline to use to invoke an external diff tool
936
if using is not None:
937
extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
940
if external_diff_options:
941
opts = external_diff_options.split()
942
def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
943
""":param path_encoding: not used but required
944
to match the signature of internal_diff.
946
external_diff(olab, olines, nlab, nlines, to_file, opts)
948
diff_file = internal_diff
949
diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
950
old_label, new_label, diff_file, context_lines=context_lines)
951
return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
954
def show_diff(self, specific_files, extra_trees=None):
955
"""Write tree diff to self.to_file
957
:param specific_files: the specific files to compare (recursive)
958
:param extra_trees: extra trees to use for mapping paths to file_ids
961
return self._show_diff(specific_files, extra_trees)
963
for differ in self.differs:
966
def _show_diff(self, specific_files, extra_trees):
967
# TODO: Generation of pseudo-diffs for added/deleted files could
968
# be usefully made into a much faster special case.
969
iterator = self.new_tree.iter_changes(self.old_tree,
970
specific_files=specific_files,
971
extra_trees=extra_trees,
972
require_versioned=True)
974
def changes_key(change):
975
old_path, new_path = change[1]
980
def get_encoded_path(path):
982
return path.encode(self.path_encoding, "replace")
983
for (file_id, paths, changed_content, versioned, parent, name, kind,
984
executable) in sorted(iterator, key=changes_key):
985
# The root does not get diffed, and items with no known kind (that
986
# is, missing) in both trees are skipped as well.
987
if parent == (None, None) or kind == (None, None):
989
oldpath, newpath = paths
990
oldpath_encoded = get_encoded_path(paths[0])
991
newpath_encoded = get_encoded_path(paths[1])
992
old_present = (kind[0] is not None and versioned[0])
993
new_present = (kind[1] is not None and versioned[1])
994
renamed = (parent[0], name[0]) != (parent[1], name[1])
996
properties_changed = []
997
properties_changed.extend(get_executable_change(executable[0], executable[1]))
999
if properties_changed:
1000
prop_str = b" (properties changed: %s)" % (
1001
b", ".join(properties_changed),)
1005
if (old_present, new_present) == (True, False):
1006
self.to_file.write(b"=== removed %s '%s'\n" %
1007
(kind[0].encode('ascii'), oldpath_encoded))
1009
elif (old_present, new_present) == (False, True):
1010
self.to_file.write(b"=== added %s '%s'\n" %
1011
(kind[1].encode('ascii'), newpath_encoded))
1014
self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1015
(kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1017
# if it was produced by iter_changes, it must be
1018
# modified *somehow*, either content or execute bit.
1019
self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1020
newpath_encoded, prop_str))
1022
self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
1028
def diff(self, file_id, old_path, new_path):
1029
"""Perform a diff of a single file
1031
:param file_id: file-id of the file
1032
:param old_path: The path of the file in the old tree
1033
:param new_path: The path of the file in the new tree
1035
if old_path is None:
1038
old_kind = self.old_tree.kind(old_path, file_id)
1039
if new_path is None:
1042
new_kind = self.new_tree.kind(new_path, file_id)
1043
self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1045
def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
1046
result = DiffPath._diff_many(self.differs, file_id, old_path,
1047
new_path, old_kind, new_kind)
1048
if result is DiffPath.CANNOT_DIFF:
1049
error_path = new_path
1050
if error_path is None:
1051
error_path = old_path
1052
raise errors.NoDiffFound(error_path)
1055
format_registry = Registry()
1056
format_registry.register('default', DiffTree)
461
mutter("check paths: %r", paths)
464
s = old_tree.filter_unversioned_files(paths)
465
s = new_tree.filter_unversioned_files(s)
466
s = [path for path in s if not new_tree.has_filename(path)]
468
raise errors.PathsDoNotExist(sorted(s))
471
def get_prop_change(meta_modified):
473
return " (properties changed)"
478
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
479
new_path, new_tree, text_modified,
480
kind, to_file, diff_file):
482
new_entry = new_tree.inventory[file_id]
483
old_tree.inventory[file_id].diff(diff_file,