~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2007-06-18 05:22:35 UTC
  • mfrom: (1551.15.27 Aaron's mergeable stuff)
  • Revision ID: pqm@pqm.ubuntu.com-20070618052235-mvns8j28szyzscy0
Turn list-weave into list-versionedfile

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
import difflib
18
17
import os
19
18
import re
20
 
import shutil
21
19
import sys
22
20
 
23
21
from bzrlib.lazy_import import lazy_import
28
26
import time
29
27
 
30
28
from bzrlib import (
31
 
    bzrdir,
32
 
    commands,
33
29
    errors,
34
30
    osutils,
35
31
    patiencediff,
38
34
    )
39
35
""")
40
36
 
 
37
# compatability - plugins import compare_trees from diff!!!
 
38
# deprecated as of 0.10
 
39
from bzrlib.delta import compare_trees
41
40
from bzrlib.symbol_versioning import (
42
41
        deprecated_function,
43
 
        one_zero,
 
42
        zero_eight,
44
43
        )
45
44
from bzrlib.trace import mutter, warning
46
45
 
49
48
# invoke callbacks on an object.  That object can either accumulate a
50
49
# list, write them out directly, etc etc.
51
50
 
52
 
 
53
 
class _PrematchedMatcher(difflib.SequenceMatcher):
54
 
    """Allow SequenceMatcher operations to use predetermined blocks"""
55
 
 
56
 
    def __init__(self, matching_blocks):
57
 
        difflib.SequenceMatcher(self, None, None)
58
 
        self.matching_blocks = matching_blocks
59
 
        self.opcodes = None
60
 
 
61
 
 
62
51
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
63
52
                  allow_binary=False, sequence_matcher=None,
64
53
                  path_encoding='utf8'):
90
79
                      sequencematcher=sequence_matcher)
91
80
 
92
81
    ud = list(ud)
93
 
    if len(ud) == 0: # Identical contents, nothing to do
94
 
        return
95
82
    # work-around for difflib being too smart for its own good
96
83
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
97
84
    if not oldlines:
106
93
        to_file.write(line)
107
94
        if not line.endswith('\n'):
108
95
            to_file.write("\n\\ No newline at end of file\n")
109
 
    to_file.write('\n')
 
96
    print >>to_file
110
97
 
111
98
 
112
99
def _spawn_external_diff(diffcmd, capture_errors=True):
274
261
                        new_abspath, e)
275
262
 
276
263
 
277
 
@deprecated_function(one_zero)
 
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.
 
268
 
 
269
    Please use show_diff_trees instead.
 
270
 
 
271
    b
 
272
        Branch.
 
273
 
 
274
    revision
 
275
        None for 'basis tree', or otherwise the old revision to compare against.
 
276
    
 
277
    The more general form is show_diff_trees(), where the caller
 
278
    supplies any two trees.
 
279
    """
 
280
    if output is None:
 
281
        output = sys.stdout
 
282
 
 
283
    if from_spec is None:
 
284
        old_tree = b.bzrdir.open_workingtree()
 
285
        if b2 is None:
 
286
            old_tree = old_tree = old_tree.basis_tree()
 
287
    else:
 
288
        old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
 
289
 
 
290
    if revision2 is None:
 
291
        if b2 is None:
 
292
            new_tree = b.bzrdir.open_workingtree()
 
293
        else:
 
294
            new_tree = b2.bzrdir.open_workingtree()
 
295
    else:
 
296
        new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
 
297
 
 
298
    return show_diff_trees(old_tree, new_tree, output, specific_files,
 
299
                           external_diff_options)
 
300
 
 
301
 
278
302
def diff_cmd_helper(tree, specific_files, external_diff_options, 
279
303
                    old_revision_spec=None, new_revision_spec=None,
280
304
                    revision_specs=None,
351
375
                           extra_trees=extra_trees)
352
376
 
353
377
 
354
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
355
 
    """Get the trees and specific files to diff given a list of paths.
356
 
 
357
 
    This method works out the trees to be diff'ed and the files of
358
 
    interest within those trees.
359
 
 
360
 
    :param path_list:
361
 
        the list of arguments passed to the diff command
362
 
    :param revision_specs:
363
 
        Zero, one or two RevisionSpecs from the diff command line,
364
 
        saying what revisions to compare.
365
 
    :param old_url:
366
 
        The url of the old branch or tree. If None, the tree to use is
367
 
        taken from the first path, if any, or the current working tree.
368
 
    :param new_url:
369
 
        The url of the new branch or tree. If None, the tree to use is
370
 
        taken from the first path, if any, or the current working tree.
371
 
    :returns:
372
 
        a tuple of (old_tree, new_tree, specific_files, extra_trees) where
373
 
        extra_trees is a sequence of additional trees to search in for
374
 
        file-ids.
375
 
    """
376
 
    # Get the old and new revision specs
377
 
    old_revision_spec = None
378
 
    new_revision_spec = None
379
 
    if revision_specs is not None:
380
 
        if len(revision_specs) > 0:
381
 
            old_revision_spec = revision_specs[0]
382
 
            if old_url is None:
383
 
                old_url = old_revision_spec.get_branch()
384
 
        if len(revision_specs) > 1:
385
 
            new_revision_spec = revision_specs[1]
386
 
            if new_url is None:
387
 
                new_url = new_revision_spec.get_branch()
388
 
 
389
 
    other_paths = []
390
 
    make_paths_wt_relative = True
391
 
    if path_list is None or len(path_list) == 0:
392
 
        # If no path is given, assume the current directory
393
 
        default_location = u'.'
394
 
    elif old_url is not None and new_url is not None:
395
 
        other_paths = path_list
396
 
        make_paths_wt_relative = False
397
 
    else:
398
 
        default_location = path_list[0]
399
 
        other_paths = path_list[1:]
400
 
 
401
 
    # Get the old location
402
 
    specific_files = []
403
 
    if old_url is None:
404
 
        old_url = default_location
405
 
    working_tree, branch, relpath = \
406
 
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
407
 
    if relpath != '':
408
 
        specific_files.append(relpath)
409
 
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
410
 
 
411
 
    # Get the new location
412
 
    if new_url is None:
413
 
        new_url = default_location
414
 
    if new_url != old_url:
415
 
        working_tree, branch, relpath = \
416
 
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
417
 
        if relpath != '':
418
 
            specific_files.append(relpath)
419
 
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
420
 
        basis_is_default=working_tree is None)
421
 
 
422
 
    # Get the specific files (all files is None, no files is [])
423
 
    if make_paths_wt_relative and working_tree is not None:
424
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
425
 
    specific_files.extend(other_paths)
426
 
    if len(specific_files) == 0:
427
 
        specific_files = None
428
 
 
429
 
    # Get extra trees that ought to be searched for file-ids
430
 
    extra_trees = None
431
 
    if working_tree is not None and working_tree not in (old_tree, new_tree):
432
 
        extra_trees = (working_tree,)
433
 
    return old_tree, new_tree, specific_files, extra_trees
434
 
 
435
 
 
436
 
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
437
 
    if branch is None and tree is not None:
438
 
        branch = tree.branch
439
 
    if spec is None or spec.spec is None:
440
 
        if basis_is_default:
441
 
            if tree is not None:
442
 
                return tree.basis_tree()
443
 
            else:
444
 
                return branch.basis_tree()
445
 
        else:
446
 
            return tree
447
 
    revision = spec.in_store(branch)
448
 
    revision_id = revision.rev_id
449
 
    rev_branch = revision.branch
450
 
    return rev_branch.repository.revision_tree(revision_id)
451
 
 
452
 
 
453
 
def _relative_paths_in_tree(tree, paths):
454
 
    """Get the relative paths within a working tree.
455
 
 
456
 
    Each path may be either an absolute path or a path relative to the
457
 
    current working directory.
458
 
    """
459
 
    result = []
460
 
    for filename in paths:
461
 
        try:
462
 
            result.append(tree.relpath(osutils.dereference_path(filename)))
463
 
        except errors.PathNotChild:
464
 
            raise errors.BzrCommandError("Files are in different branches")
465
 
    return result
466
 
 
467
 
 
468
378
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
469
379
                    external_diff_options=None,
470
380
                    old_label='a/', new_label='b/',
471
 
                    extra_trees=None,
472
 
                    path_encoding='utf8',
473
 
                    using=None):
 
381
                    extra_trees=None):
474
382
    """Show in text form the changes from one tree to another.
475
383
 
476
 
    to_file
477
 
        The output stream.
478
 
 
479
 
    specific_files
480
 
        Include only changes to these files - None for all changes.
 
384
    to_files
 
385
        If set, include only changes to these files.
481
386
 
482
387
    external_diff_options
483
388
        If set, use an external GNU diff and pass these options.
484
389
 
485
390
    extra_trees
486
391
        If set, more Trees to use for looking up file ids
487
 
 
488
 
    path_encoding
489
 
        If set, the path will be encoded as specified, otherwise is supposed
490
 
        to be utf8
491
392
    """
492
393
    old_tree.lock_read()
493
394
    try:
496
397
                tree.lock_read()
497
398
        new_tree.lock_read()
498
399
        try:
499
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
500
 
                                                 path_encoding,
501
 
                                                 external_diff_options,
502
 
                                                 old_label, new_label, using)
503
 
            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)
504
404
        finally:
505
405
            new_tree.unlock()
506
406
            if extra_trees is not None:
510
410
        old_tree.unlock()
511
411
 
512
412
 
 
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):
 
416
 
 
417
    # GNU Patch uses the epoch date to detect files that are being added
 
418
    # or removed in a diff.
 
419
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
 
420
 
 
421
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
422
    # be usefully made into a much faster special case.
 
423
 
 
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)
 
429
    else:
 
430
        diff_file = internal_diff
 
431
    
 
432
    delta = new_tree.changes_from(old_tree,
 
433
        specific_files=specific_files,
 
434
        extra_trees=extra_trees, require_versioned=True)
 
435
 
 
436
    has_changes = 0
 
437
    for path, file_id, kind in delta.removed:
 
438
        has_changes = 1
 
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:
 
446
        has_changes = 1
 
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, 
 
453
                                         reverse=True)
 
454
    for (old_path, new_path, file_id, kind,
 
455
         text_modified, meta_modified) in delta.renamed:
 
456
        has_changes = 1
 
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,
 
463
                                                    old_path))
 
464
        new_name = '%s%s\t%s' % (new_label, new_path,
 
465
                                 _patch_header_date(new_tree, file_id,
 
466
                                                    new_path))
 
467
        _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
468
                                    new_name, new_tree,
 
469
                                    text_modified, kind, to_file, diff_file)
 
470
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
 
471
        has_changes = 1
 
472
        prop_str = get_prop_change(meta_modified)
 
473
        print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
 
474
        # The file may be in a different location in the old tree (because
 
475
        # the containing dir was renamed, but the file itself was not)
 
476
        old_path = old_tree.id2path(file_id)
 
477
        old_name = '%s%s\t%s' % (old_label, old_path,
 
478
                                 _patch_header_date(old_tree, file_id, old_path))
 
479
        new_name = '%s%s\t%s' % (new_label, path,
 
480
                                 _patch_header_date(new_tree, file_id, path))
 
481
        if text_modified:
 
482
            _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
483
                                        new_name, new_tree,
 
484
                                        True, kind, to_file, diff_file)
 
485
 
 
486
    return has_changes
 
487
 
 
488
 
513
489
def _patch_header_date(tree, file_id, path):
514
490
    """Returns a timestamp suitable for use in a patch header."""
515
491
    mtime = tree.get_file_mtime(file_id, path)
545
521
        return  ""
546
522
 
547
523
 
548
 
class DiffPath(object):
549
 
    """Base type for command object that compare files"""
550
 
 
551
 
    # The type or contents of the file were unsuitable for diffing
552
 
    CANNOT_DIFF = 'CANNOT_DIFF'
553
 
    # The file has changed in a semantic way
554
 
    CHANGED = 'CHANGED'
555
 
    # The file content may have changed, but there is no semantic change
556
 
    UNCHANGED = 'UNCHANGED'
557
 
 
558
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
559
 
        """Constructor.
560
 
 
561
 
        :param old_tree: The tree to show as the old tree in the comparison
562
 
        :param new_tree: The tree to show as new in the comparison
563
 
        :param to_file: The file to write comparison data to
564
 
        :param path_encoding: The character encoding to write paths in
565
 
        """
566
 
        self.old_tree = old_tree
567
 
        self.new_tree = new_tree
568
 
        self.to_file = to_file
569
 
        self.path_encoding = path_encoding
570
 
 
571
 
    def finish(self):
572
 
        pass
573
 
 
574
 
    @classmethod
575
 
    def from_diff_tree(klass, diff_tree):
576
 
        return klass(diff_tree.old_tree, diff_tree.new_tree,
577
 
                     diff_tree.to_file, diff_tree.path_encoding)
578
 
 
579
 
    @staticmethod
580
 
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
581
 
        for file_differ in differs:
582
 
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
583
 
                                      new_kind)
584
 
            if result is not DiffPath.CANNOT_DIFF:
585
 
                return result
586
 
        else:
587
 
            return DiffPath.CANNOT_DIFF
588
 
 
589
 
 
590
 
class DiffKindChange(object):
591
 
    """Special differ for file kind changes.
592
 
 
593
 
    Represents kind change as deletion + creation.  Uses the other differs
594
 
    to do this.
595
 
    """
596
 
    def __init__(self, differs):
597
 
        self.differs = differs
598
 
 
599
 
    def finish(self):
600
 
        pass
601
 
 
602
 
    @classmethod
603
 
    def from_diff_tree(klass, diff_tree):
604
 
        return klass(diff_tree.differs)
605
 
 
606
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
607
 
        """Perform comparison
608
 
 
609
 
        :param file_id: The file_id of the file to compare
610
 
        :param old_path: Path of the file in the old tree
611
 
        :param new_path: Path of the file in the new tree
612
 
        :param old_kind: Old file-kind of the file
613
 
        :param new_kind: New file-kind of the file
614
 
        """
615
 
        if None in (old_kind, new_kind):
616
 
            return DiffPath.CANNOT_DIFF
617
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
618
 
                                       new_path, old_kind, None)
619
 
        if result is DiffPath.CANNOT_DIFF:
620
 
            return result
621
 
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
622
 
                                     None, new_kind)
623
 
 
624
 
 
625
 
class DiffDirectory(DiffPath):
626
 
 
627
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
628
 
        """Perform comparison between two directories.  (dummy)
629
 
 
630
 
        """
631
 
        if 'directory' not in (old_kind, new_kind):
632
 
            return self.CANNOT_DIFF
633
 
        if old_kind not in ('directory', None):
634
 
            return self.CANNOT_DIFF
635
 
        if new_kind not in ('directory', None):
636
 
            return self.CANNOT_DIFF
637
 
        return self.CHANGED
638
 
 
639
 
 
640
 
class DiffSymlink(DiffPath):
641
 
 
642
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
643
 
        """Perform comparison between two symlinks
644
 
 
645
 
        :param file_id: The file_id of the file to compare
646
 
        :param old_path: Path of the file in the old tree
647
 
        :param new_path: Path of the file in the new tree
648
 
        :param old_kind: Old file-kind of the file
649
 
        :param new_kind: New file-kind of the file
650
 
        """
651
 
        if 'symlink' not in (old_kind, new_kind):
652
 
            return self.CANNOT_DIFF
653
 
        if old_kind == 'symlink':
654
 
            old_target = self.old_tree.get_symlink_target(file_id)
655
 
        elif old_kind is None:
656
 
            old_target = None
657
 
        else:
658
 
            return self.CANNOT_DIFF
659
 
        if new_kind == 'symlink':
660
 
            new_target = self.new_tree.get_symlink_target(file_id)
661
 
        elif new_kind is None:
662
 
            new_target = None
663
 
        else:
664
 
            return self.CANNOT_DIFF
665
 
        return self.diff_symlink(old_target, new_target)
666
 
 
667
 
    def diff_symlink(self, old_target, new_target):
668
 
        if old_target is None:
669
 
            self.to_file.write('=== target is %r\n' % new_target)
670
 
        elif new_target is None:
671
 
            self.to_file.write('=== target was %r\n' % old_target)
672
 
        else:
673
 
            self.to_file.write('=== target changed %r => %r\n' %
674
 
                              (old_target, new_target))
675
 
        return self.CHANGED
676
 
 
677
 
 
678
 
class DiffText(DiffPath):
679
 
 
680
 
    # GNU Patch uses the epoch date to detect files that are being added
681
 
    # or removed in a diff.
682
 
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
683
 
 
684
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
685
 
                 old_label='', new_label='', text_differ=internal_diff):
686
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
687
 
        self.text_differ = text_differ
688
 
        self.old_label = old_label
689
 
        self.new_label = new_label
690
 
        self.path_encoding = path_encoding
691
 
 
692
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
693
 
        """Compare two files in unified diff format
694
 
 
695
 
        :param file_id: The file_id of the file to compare
696
 
        :param old_path: Path of the file in the old tree
697
 
        :param new_path: Path of the file in the new tree
698
 
        :param old_kind: Old file-kind of the file
699
 
        :param new_kind: New file-kind of the file
700
 
        """
701
 
        if 'file' not in (old_kind, new_kind):
702
 
            return self.CANNOT_DIFF
703
 
        from_file_id = to_file_id = file_id
704
 
        if old_kind == 'file':
705
 
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
706
 
        elif old_kind is None:
707
 
            old_date = self.EPOCH_DATE
708
 
            from_file_id = None
709
 
        else:
710
 
            return self.CANNOT_DIFF
711
 
        if new_kind == 'file':
712
 
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
713
 
        elif new_kind is None:
714
 
            new_date = self.EPOCH_DATE
715
 
            to_file_id = None
716
 
        else:
717
 
            return self.CANNOT_DIFF
718
 
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
719
 
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
720
 
        return self.diff_text(from_file_id, to_file_id, from_label, to_label)
721
 
 
722
 
    def diff_text(self, from_file_id, to_file_id, from_label, to_label):
723
 
        """Diff the content of given files in two trees
724
 
 
725
 
        :param from_file_id: The id of the file in the from tree.  If None,
726
 
            the file is not present in the from tree.
727
 
        :param to_file_id: The id of the file in the to tree.  This may refer
728
 
            to a different file from from_file_id.  If None,
729
 
            the file is not present in the to tree.
730
 
        """
731
 
        def _get_text(tree, file_id):
732
 
            if file_id is not None:
733
 
                return tree.get_file(file_id).readlines()
734
 
            else:
735
 
                return []
736
 
        try:
737
 
            from_text = _get_text(self.old_tree, from_file_id)
738
 
            to_text = _get_text(self.new_tree, to_file_id)
739
 
            self.text_differ(from_label, from_text, to_label, to_text,
740
 
                             self.to_file)
741
 
        except errors.BinaryFile:
742
 
            self.to_file.write(
743
 
                  ("Binary files %s and %s differ\n" %
744
 
                  (from_label, to_label)).encode(self.path_encoding))
745
 
        return self.CHANGED
746
 
 
747
 
 
748
 
class DiffFromTool(DiffPath):
749
 
 
750
 
    def __init__(self, command_template, old_tree, new_tree, to_file,
751
 
                 path_encoding='utf-8'):
752
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
753
 
        self.command_template = command_template
754
 
        self._root = tempfile.mkdtemp(prefix='bzr-diff-')
755
 
 
756
 
    @classmethod
757
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
758
 
                    path_encoding='utf-8'):
759
 
        command_template = commands.shlex_split_unicode(command_string)
760
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
761
 
        return klass(command_template, old_tree, new_tree, to_file,
762
 
                     path_encoding)
763
 
 
764
 
    @classmethod
765
 
    def make_from_diff_tree(klass, command_string):
766
 
        def from_diff_tree(diff_tree):
767
 
            return klass.from_string(command_string, diff_tree.old_tree,
768
 
                                     diff_tree.new_tree, diff_tree.to_file)
769
 
        return from_diff_tree
770
 
 
771
 
    def _get_command(self, old_path, new_path):
772
 
        my_map = {'old_path': old_path, 'new_path': new_path}
773
 
        return [t % my_map for t in self.command_template]
774
 
 
775
 
    def _execute(self, old_path, new_path):
776
 
        proc = subprocess.Popen(self._get_command(old_path, new_path),
777
 
                                stdout=subprocess.PIPE, cwd=self._root)
778
 
        self.to_file.write(proc.stdout.read())
779
 
        return proc.wait()
780
 
 
781
 
    def _write_file(self, file_id, tree, prefix, old_path):
782
 
        full_old_path = osutils.pathjoin(self._root, prefix, old_path)
783
 
        parent_dir = osutils.dirname(full_old_path)
784
 
        try:
785
 
            os.makedirs(parent_dir)
786
 
        except OSError, e:
787
 
            if e.errno != errno.EEXIST:
788
 
                raise
789
 
        source = tree.get_file(file_id)
790
 
        try:
791
 
            target = open(full_old_path, 'wb')
792
 
            try:
793
 
                osutils.pumpfile(source, target)
794
 
            finally:
795
 
                target.close()
796
 
        finally:
797
 
            source.close()
798
 
        return full_old_path
799
 
 
800
 
    def _prepare_files(self, file_id, old_path, new_path):
801
 
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
802
 
                                         old_path)
803
 
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
804
 
                                         new_path)
805
 
        return old_disk_path, new_disk_path
806
 
 
807
 
    def finish(self):
808
 
        shutil.rmtree(self._root)
809
 
 
810
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
811
 
        if (old_kind, new_kind) != ('file', 'file'):
812
 
            return DiffPath.CANNOT_DIFF
813
 
        self._prepare_files(file_id, old_path, new_path)
814
 
        self._execute(osutils.pathjoin('old', old_path),
815
 
                      osutils.pathjoin('new', new_path))
816
 
 
817
 
 
818
 
class DiffTree(object):
819
 
    """Provides textual representations of the difference between two trees.
820
 
 
821
 
    A DiffTree examines two trees and where a file-id has altered
822
 
    between them, generates a textual representation of the difference.
823
 
    DiffTree uses a sequence of DiffPath objects which are each
824
 
    given the opportunity to handle a given altered fileid. The list
825
 
    of DiffPath objects can be extended globally by appending to
826
 
    DiffTree.diff_factories, or for a specific diff operation by
827
 
    supplying the extra_factories option to the appropriate method.
828
 
    """
829
 
 
830
 
    # list of factories that can provide instances of DiffPath objects
831
 
    # may be extended by plugins.
832
 
    diff_factories = [DiffSymlink.from_diff_tree,
833
 
                      DiffDirectory.from_diff_tree]
834
 
 
835
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
836
 
                 diff_text=None, extra_factories=None):
837
 
        """Constructor
838
 
 
839
 
        :param old_tree: Tree to show as old in the comparison
840
 
        :param new_tree: Tree to show as new in the comparison
841
 
        :param to_file: File to write comparision to
842
 
        :param path_encoding: Character encoding to write paths in
843
 
        :param diff_text: DiffPath-type object to use as a last resort for
844
 
            diffing text files.
845
 
        :param extra_factories: Factories of DiffPaths to try before any other
846
 
            DiffPaths"""
847
 
        if diff_text is None:
848
 
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
849
 
                                 '', '',  internal_diff)
850
 
        self.old_tree = old_tree
851
 
        self.new_tree = new_tree
852
 
        self.to_file = to_file
853
 
        self.path_encoding = path_encoding
854
 
        self.differs = []
855
 
        if extra_factories is not None:
856
 
            self.differs.extend(f(self) for f in extra_factories)
857
 
        self.differs.extend(f(self) for f in self.diff_factories)
858
 
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
859
 
 
860
 
    @classmethod
861
 
    def from_trees_options(klass, old_tree, new_tree, to_file,
862
 
                           path_encoding, external_diff_options, old_label,
863
 
                           new_label, using):
864
 
        """Factory for producing a DiffTree.
865
 
 
866
 
        Designed to accept options used by show_diff_trees.
867
 
        :param old_tree: The tree to show as old in the comparison
868
 
        :param new_tree: The tree to show as new in the comparison
869
 
        :param to_file: File to write comparisons to
870
 
        :param path_encoding: Character encoding to use for writing paths
871
 
        :param external_diff_options: If supplied, use the installed diff
872
 
            binary to perform file comparison, using supplied options.
873
 
        :param old_label: Prefix to use for old file labels
874
 
        :param new_label: Prefix to use for new file labels
875
 
        :param using: Commandline to use to invoke an external diff tool
876
 
        """
877
 
        if using is not None:
878
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using)]
879
 
        else:
880
 
            extra_factories = []
881
 
        if external_diff_options:
882
 
            assert isinstance(external_diff_options, basestring)
883
 
            opts = external_diff_options.split()
884
 
            def diff_file(olab, olines, nlab, nlines, to_file):
885
 
                external_diff(olab, olines, nlab, nlines, to_file, opts)
886
 
        else:
887
 
            diff_file = internal_diff
888
 
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
889
 
                             old_label, new_label, diff_file)
890
 
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
891
 
                     extra_factories)
892
 
 
893
 
    def show_diff(self, specific_files, extra_trees=None):
894
 
        """Write tree diff to self.to_file
895
 
 
896
 
        :param sepecific_files: the specific files to compare (recursive)
897
 
        :param extra_trees: extra trees to use for mapping paths to file_ids
898
 
        """
899
 
        try:
900
 
            return self._show_diff(specific_files, extra_trees)
901
 
        finally:
902
 
            for differ in self.differs:
903
 
                differ.finish()
904
 
 
905
 
    def _show_diff(self, specific_files, extra_trees):
906
 
        # TODO: Generation of pseudo-diffs for added/deleted files could
907
 
        # be usefully made into a much faster special case.
908
 
        iterator = self.new_tree._iter_changes(self.old_tree,
909
 
                                               specific_files=specific_files,
910
 
                                               extra_trees=extra_trees,
911
 
                                               require_versioned=True)
912
 
        has_changes = 0
913
 
        def changes_key(change):
914
 
            old_path, new_path = change[1]
915
 
            path = new_path
916
 
            if path is None:
917
 
                path = old_path
918
 
            return path
919
 
        def get_encoded_path(path):
920
 
            if path is not None:
921
 
                return path.encode(self.path_encoding, "replace")
922
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
923
 
             executable) in sorted(iterator, key=changes_key):
924
 
            if parent == (None, None):
925
 
                continue
926
 
            oldpath, newpath = paths
927
 
            oldpath_encoded = get_encoded_path(paths[0])
928
 
            newpath_encoded = get_encoded_path(paths[1])
929
 
            old_present = (kind[0] is not None and versioned[0])
930
 
            new_present = (kind[1] is not None and versioned[1])
931
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
932
 
            prop_str = get_prop_change(executable[0] != executable[1])
933
 
            if (old_present, new_present) == (True, False):
934
 
                self.to_file.write("=== removed %s '%s'\n" %
935
 
                                   (kind[0], oldpath_encoded))
936
 
                newpath = oldpath
937
 
            elif (old_present, new_present) == (False, True):
938
 
                self.to_file.write("=== added %s '%s'\n" %
939
 
                                   (kind[1], newpath_encoded))
940
 
                oldpath = newpath
941
 
            elif renamed:
942
 
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
943
 
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
944
 
            else:
945
 
                # if it was produced by _iter_changes, it must be
946
 
                # modified *somehow*, either content or execute bit.
947
 
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
948
 
                                   newpath_encoded, prop_str))
949
 
            if changed_content:
950
 
                self.diff(file_id, oldpath, newpath)
951
 
                has_changes = 1
952
 
            if renamed:
953
 
                has_changes = 1
954
 
        return has_changes
955
 
 
956
 
    def diff(self, file_id, old_path, new_path):
957
 
        """Perform a diff of a single file
958
 
 
959
 
        :param file_id: file-id of the file
960
 
        :param old_path: The path of the file in the old tree
961
 
        :param new_path: The path of the file in the new tree
962
 
        """
963
 
        try:
964
 
            old_kind = self.old_tree.kind(file_id)
965
 
        except (errors.NoSuchId, errors.NoSuchFile):
966
 
            old_kind = None
967
 
        try:
968
 
            new_kind = self.new_tree.kind(file_id)
969
 
        except (errors.NoSuchId, errors.NoSuchFile):
970
 
            new_kind = None
971
 
 
972
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
973
 
                                       new_path, old_kind, new_kind)
974
 
        if result is DiffPath.CANNOT_DIFF:
975
 
            error_path = new_path
976
 
            if error_path is None:
977
 
                error_path = old_path
978
 
            raise errors.NoDiffFound(error_path)
 
524
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
 
525
                                new_path, new_tree, text_modified,
 
526
                                kind, to_file, diff_file):
 
527
    if text_modified:
 
528
        new_entry = new_tree.inventory[file_id]
 
529
        old_tree.inventory[file_id].diff(diff_file,
 
530
                                         old_path, old_tree,
 
531
                                         new_path, new_entry, 
 
532
                                         new_tree, to_file)