~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Alexander Belchenko
  • Date: 2007-08-10 09:04:38 UTC
  • mto: This revision was merged to the branch mainline in revision 2694.
  • Revision ID: bialix@ukr.net-20070810090438-0835xdz0rl8825qv
fixes after Ian's review

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
 
    consider_relpath = True
392
 
    if path_list is None or len(path_list) == 0:
393
 
        # If no path is given, the current working tree is used
394
 
        default_location = u'.'
395
 
        consider_relpath = False
396
 
    elif old_url is not None and new_url is not None:
397
 
        other_paths = path_list
398
 
        make_paths_wt_relative = False
399
 
    else:
400
 
        default_location = path_list[0]
401
 
        other_paths = path_list[1:]
402
 
 
403
 
    # Get the old location
404
 
    specific_files = []
405
 
    if old_url is None:
406
 
        old_url = default_location
407
 
    working_tree, branch, relpath = \
408
 
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
409
 
    if consider_relpath and relpath != '':
410
 
        specific_files.append(relpath)
411
 
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
412
 
 
413
 
    # Get the new location
414
 
    if new_url is None:
415
 
        new_url = default_location
416
 
    if new_url != old_url:
417
 
        working_tree, branch, relpath = \
418
 
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
419
 
        if consider_relpath and relpath != '':
420
 
            specific_files.append(relpath)
421
 
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
422
 
        basis_is_default=working_tree is None)
423
 
 
424
 
    # Get the specific files (all files is None, no files is [])
425
 
    if make_paths_wt_relative and working_tree is not None:
426
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
427
 
    specific_files.extend(other_paths)
428
 
    if len(specific_files) == 0:
429
 
        specific_files = None
430
 
 
431
 
    # Get extra trees that ought to be searched for file-ids
432
 
    extra_trees = None
433
 
    if working_tree is not None and working_tree not in (old_tree, new_tree):
434
 
        extra_trees = (working_tree,)
435
 
    return old_tree, new_tree, specific_files, extra_trees
436
 
 
437
 
 
438
 
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
439
 
    if branch is None and tree is not None:
440
 
        branch = tree.branch
441
 
    if spec is None or spec.spec is None:
442
 
        if basis_is_default:
443
 
            if tree is not None:
444
 
                return tree.basis_tree()
445
 
            else:
446
 
                return branch.basis_tree()
447
 
        else:
448
 
            return tree
449
 
    revision = spec.in_store(branch)
450
 
    revision_id = revision.rev_id
451
 
    rev_branch = revision.branch
452
 
    return rev_branch.repository.revision_tree(revision_id)
453
 
 
454
 
 
455
 
def _relative_paths_in_tree(tree, paths):
456
 
    """Get the relative paths within a working tree.
457
 
 
458
 
    Each path may be either an absolute path or a path relative to the
459
 
    current working directory.
460
 
    """
461
 
    result = []
462
 
    for filename in paths:
463
 
        try:
464
 
            result.append(tree.relpath(osutils.dereference_path(filename)))
465
 
        except errors.PathNotChild:
466
 
            raise errors.BzrCommandError("Files are in different branches")
467
 
    return result
468
 
 
469
 
 
470
378
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
471
379
                    external_diff_options=None,
472
380
                    old_label='a/', new_label='b/',
473
 
                    extra_trees=None,
474
 
                    path_encoding='utf8',
475
 
                    using=None):
 
381
                    extra_trees=None):
476
382
    """Show in text form the changes from one tree to another.
477
383
 
478
 
    to_file
479
 
        The output stream.
480
 
 
481
 
    specific_files
482
 
        Include only changes to these files - None for all changes.
 
384
    to_files
 
385
        If set, include only changes to these files.
483
386
 
484
387
    external_diff_options
485
388
        If set, use an external GNU diff and pass these options.
486
389
 
487
390
    extra_trees
488
391
        If set, more Trees to use for looking up file ids
489
 
 
490
 
    path_encoding
491
 
        If set, the path will be encoded as specified, otherwise is supposed
492
 
        to be utf8
493
392
    """
494
393
    old_tree.lock_read()
495
394
    try:
498
397
                tree.lock_read()
499
398
        new_tree.lock_read()
500
399
        try:
501
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
502
 
                                                 path_encoding,
503
 
                                                 external_diff_options,
504
 
                                                 old_label, new_label, using)
505
 
            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)
506
404
        finally:
507
405
            new_tree.unlock()
508
406
            if extra_trees is not None:
512
410
        old_tree.unlock()
513
411
 
514
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 '%s'" % (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 '%s'" % (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 '%s' => %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 '%s'%s" % (kind, path.encode('utf8'),
 
474
                                                     prop_str)
 
475
        # The file may be in a different location in the old tree (because
 
476
        # the containing dir was renamed, but the file itself was not)
 
477
        old_path = old_tree.id2path(file_id)
 
478
        old_name = '%s%s\t%s' % (old_label, old_path,
 
479
                                 _patch_header_date(old_tree, file_id, old_path))
 
480
        new_name = '%s%s\t%s' % (new_label, path,
 
481
                                 _patch_header_date(new_tree, file_id, path))
 
482
        if text_modified:
 
483
            _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
484
                                        new_name, new_tree,
 
485
                                        True, kind, to_file, diff_file)
 
486
 
 
487
    return has_changes
 
488
 
 
489
 
515
490
def _patch_header_date(tree, file_id, path):
516
491
    """Returns a timestamp suitable for use in a patch header."""
517
492
    mtime = tree.get_file_mtime(file_id, path)
547
522
        return  ""
548
523
 
549
524
 
550
 
class DiffPath(object):
551
 
    """Base type for command object that compare files"""
552
 
 
553
 
    # The type or contents of the file were unsuitable for diffing
554
 
    CANNOT_DIFF = 'CANNOT_DIFF'
555
 
    # The file has changed in a semantic way
556
 
    CHANGED = 'CHANGED'
557
 
    # The file content may have changed, but there is no semantic change
558
 
    UNCHANGED = 'UNCHANGED'
559
 
 
560
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
561
 
        """Constructor.
562
 
 
563
 
        :param old_tree: The tree to show as the old tree in the comparison
564
 
        :param new_tree: The tree to show as new in the comparison
565
 
        :param to_file: The file to write comparison data to
566
 
        :param path_encoding: The character encoding to write paths in
567
 
        """
568
 
        self.old_tree = old_tree
569
 
        self.new_tree = new_tree
570
 
        self.to_file = to_file
571
 
        self.path_encoding = path_encoding
572
 
 
573
 
    def finish(self):
574
 
        pass
575
 
 
576
 
    @classmethod
577
 
    def from_diff_tree(klass, diff_tree):
578
 
        return klass(diff_tree.old_tree, diff_tree.new_tree,
579
 
                     diff_tree.to_file, diff_tree.path_encoding)
580
 
 
581
 
    @staticmethod
582
 
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
583
 
        for file_differ in differs:
584
 
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
585
 
                                      new_kind)
586
 
            if result is not DiffPath.CANNOT_DIFF:
587
 
                return result
588
 
        else:
589
 
            return DiffPath.CANNOT_DIFF
590
 
 
591
 
 
592
 
class DiffKindChange(object):
593
 
    """Special differ for file kind changes.
594
 
 
595
 
    Represents kind change as deletion + creation.  Uses the other differs
596
 
    to do this.
597
 
    """
598
 
    def __init__(self, differs):
599
 
        self.differs = differs
600
 
 
601
 
    def finish(self):
602
 
        pass
603
 
 
604
 
    @classmethod
605
 
    def from_diff_tree(klass, diff_tree):
606
 
        return klass(diff_tree.differs)
607
 
 
608
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
609
 
        """Perform comparison
610
 
 
611
 
        :param file_id: The file_id of the file to compare
612
 
        :param old_path: Path of the file in the old tree
613
 
        :param new_path: Path of the file in the new tree
614
 
        :param old_kind: Old file-kind of the file
615
 
        :param new_kind: New file-kind of the file
616
 
        """
617
 
        if None in (old_kind, new_kind):
618
 
            return DiffPath.CANNOT_DIFF
619
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
620
 
                                       new_path, old_kind, None)
621
 
        if result is DiffPath.CANNOT_DIFF:
622
 
            return result
623
 
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
624
 
                                     None, new_kind)
625
 
 
626
 
 
627
 
class DiffDirectory(DiffPath):
628
 
 
629
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
630
 
        """Perform comparison between two directories.  (dummy)
631
 
 
632
 
        """
633
 
        if 'directory' not in (old_kind, new_kind):
634
 
            return self.CANNOT_DIFF
635
 
        if old_kind not in ('directory', None):
636
 
            return self.CANNOT_DIFF
637
 
        if new_kind not in ('directory', None):
638
 
            return self.CANNOT_DIFF
639
 
        return self.CHANGED
640
 
 
641
 
 
642
 
class DiffSymlink(DiffPath):
643
 
 
644
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
645
 
        """Perform comparison between two symlinks
646
 
 
647
 
        :param file_id: The file_id of the file to compare
648
 
        :param old_path: Path of the file in the old tree
649
 
        :param new_path: Path of the file in the new tree
650
 
        :param old_kind: Old file-kind of the file
651
 
        :param new_kind: New file-kind of the file
652
 
        """
653
 
        if 'symlink' not in (old_kind, new_kind):
654
 
            return self.CANNOT_DIFF
655
 
        if old_kind == 'symlink':
656
 
            old_target = self.old_tree.get_symlink_target(file_id)
657
 
        elif old_kind is None:
658
 
            old_target = None
659
 
        else:
660
 
            return self.CANNOT_DIFF
661
 
        if new_kind == 'symlink':
662
 
            new_target = self.new_tree.get_symlink_target(file_id)
663
 
        elif new_kind is None:
664
 
            new_target = None
665
 
        else:
666
 
            return self.CANNOT_DIFF
667
 
        return self.diff_symlink(old_target, new_target)
668
 
 
669
 
    def diff_symlink(self, old_target, new_target):
670
 
        if old_target is None:
671
 
            self.to_file.write('=== target is %r\n' % new_target)
672
 
        elif new_target is None:
673
 
            self.to_file.write('=== target was %r\n' % old_target)
674
 
        else:
675
 
            self.to_file.write('=== target changed %r => %r\n' %
676
 
                              (old_target, new_target))
677
 
        return self.CHANGED
678
 
 
679
 
 
680
 
class DiffText(DiffPath):
681
 
 
682
 
    # GNU Patch uses the epoch date to detect files that are being added
683
 
    # or removed in a diff.
684
 
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
685
 
 
686
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
687
 
                 old_label='', new_label='', text_differ=internal_diff):
688
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
689
 
        self.text_differ = text_differ
690
 
        self.old_label = old_label
691
 
        self.new_label = new_label
692
 
        self.path_encoding = path_encoding
693
 
 
694
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
695
 
        """Compare two files in unified diff format
696
 
 
697
 
        :param file_id: The file_id of the file to compare
698
 
        :param old_path: Path of the file in the old tree
699
 
        :param new_path: Path of the file in the new tree
700
 
        :param old_kind: Old file-kind of the file
701
 
        :param new_kind: New file-kind of the file
702
 
        """
703
 
        if 'file' not in (old_kind, new_kind):
704
 
            return self.CANNOT_DIFF
705
 
        from_file_id = to_file_id = file_id
706
 
        if old_kind == 'file':
707
 
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
708
 
        elif old_kind is None:
709
 
            old_date = self.EPOCH_DATE
710
 
            from_file_id = None
711
 
        else:
712
 
            return self.CANNOT_DIFF
713
 
        if new_kind == 'file':
714
 
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
715
 
        elif new_kind is None:
716
 
            new_date = self.EPOCH_DATE
717
 
            to_file_id = None
718
 
        else:
719
 
            return self.CANNOT_DIFF
720
 
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
721
 
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
722
 
        return self.diff_text(from_file_id, to_file_id, from_label, to_label)
723
 
 
724
 
    def diff_text(self, from_file_id, to_file_id, from_label, to_label):
725
 
        """Diff the content of given files in two trees
726
 
 
727
 
        :param from_file_id: The id of the file in the from tree.  If None,
728
 
            the file is not present in the from tree.
729
 
        :param to_file_id: The id of the file in the to tree.  This may refer
730
 
            to a different file from from_file_id.  If None,
731
 
            the file is not present in the to tree.
732
 
        """
733
 
        def _get_text(tree, file_id):
734
 
            if file_id is not None:
735
 
                return tree.get_file(file_id).readlines()
736
 
            else:
737
 
                return []
738
 
        try:
739
 
            from_text = _get_text(self.old_tree, from_file_id)
740
 
            to_text = _get_text(self.new_tree, to_file_id)
741
 
            self.text_differ(from_label, from_text, to_label, to_text,
742
 
                             self.to_file)
743
 
        except errors.BinaryFile:
744
 
            self.to_file.write(
745
 
                  ("Binary files %s and %s differ\n" %
746
 
                  (from_label, to_label)).encode(self.path_encoding))
747
 
        return self.CHANGED
748
 
 
749
 
 
750
 
class DiffFromTool(DiffPath):
751
 
 
752
 
    def __init__(self, command_template, old_tree, new_tree, to_file,
753
 
                 path_encoding='utf-8'):
754
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
755
 
        self.command_template = command_template
756
 
        self._root = tempfile.mkdtemp(prefix='bzr-diff-')
757
 
 
758
 
    @classmethod
759
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
760
 
                    path_encoding='utf-8'):
761
 
        command_template = commands.shlex_split_unicode(command_string)
762
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
763
 
        return klass(command_template, old_tree, new_tree, to_file,
764
 
                     path_encoding)
765
 
 
766
 
    @classmethod
767
 
    def make_from_diff_tree(klass, command_string):
768
 
        def from_diff_tree(diff_tree):
769
 
            return klass.from_string(command_string, diff_tree.old_tree,
770
 
                                     diff_tree.new_tree, diff_tree.to_file)
771
 
        return from_diff_tree
772
 
 
773
 
    def _get_command(self, old_path, new_path):
774
 
        my_map = {'old_path': old_path, 'new_path': new_path}
775
 
        return [t % my_map for t in self.command_template]
776
 
 
777
 
    def _execute(self, old_path, new_path):
778
 
        command = self._get_command(old_path, new_path)
779
 
        try:
780
 
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
781
 
                                    cwd=self._root)
782
 
        except OSError, e:
783
 
            if e.errno == errno.ENOENT:
784
 
                raise errors.ExecutableMissing(command[0])
785
 
            else:
786
 
                raise
787
 
        self.to_file.write(proc.stdout.read())
788
 
        return proc.wait()
789
 
 
790
 
    def _try_symlink_root(self, tree, prefix):
791
 
        if not (getattr(tree, 'abspath', None) is not None
792
 
                and osutils.has_symlinks()):
793
 
            return False
794
 
        try:
795
 
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
796
 
        except OSError, e:
797
 
            if e.errno != errno.EEXIST:
798
 
                raise
799
 
        return True
800
 
 
801
 
    def _write_file(self, file_id, tree, prefix, relpath):
802
 
        full_path = osutils.pathjoin(self._root, prefix, relpath)
803
 
        if self._try_symlink_root(tree, prefix):
804
 
            return full_path
805
 
        parent_dir = osutils.dirname(full_path)
806
 
        try:
807
 
            os.makedirs(parent_dir)
808
 
        except OSError, e:
809
 
            if e.errno != errno.EEXIST:
810
 
                raise
811
 
        source = tree.get_file(file_id, relpath)
812
 
        try:
813
 
            target = open(full_path, 'wb')
814
 
            try:
815
 
                osutils.pumpfile(source, target)
816
 
            finally:
817
 
                target.close()
818
 
        finally:
819
 
            source.close()
820
 
        osutils.make_readonly(full_path)
821
 
        mtime = tree.get_file_mtime(file_id)
822
 
        os.utime(full_path, (mtime, mtime))
823
 
        return full_path
824
 
 
825
 
    def _prepare_files(self, file_id, old_path, new_path):
826
 
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
827
 
                                         old_path)
828
 
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
829
 
                                         new_path)
830
 
        return old_disk_path, new_disk_path
831
 
 
832
 
    def finish(self):
833
 
        osutils.rmtree(self._root)
834
 
 
835
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
836
 
        if (old_kind, new_kind) != ('file', 'file'):
837
 
            return DiffPath.CANNOT_DIFF
838
 
        self._prepare_files(file_id, old_path, new_path)
839
 
        self._execute(osutils.pathjoin('old', old_path),
840
 
                      osutils.pathjoin('new', new_path))
841
 
 
842
 
 
843
 
class DiffTree(object):
844
 
    """Provides textual representations of the difference between two trees.
845
 
 
846
 
    A DiffTree examines two trees and where a file-id has altered
847
 
    between them, generates a textual representation of the difference.
848
 
    DiffTree uses a sequence of DiffPath objects which are each
849
 
    given the opportunity to handle a given altered fileid. The list
850
 
    of DiffPath objects can be extended globally by appending to
851
 
    DiffTree.diff_factories, or for a specific diff operation by
852
 
    supplying the extra_factories option to the appropriate method.
853
 
    """
854
 
 
855
 
    # list of factories that can provide instances of DiffPath objects
856
 
    # may be extended by plugins.
857
 
    diff_factories = [DiffSymlink.from_diff_tree,
858
 
                      DiffDirectory.from_diff_tree]
859
 
 
860
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
861
 
                 diff_text=None, extra_factories=None):
862
 
        """Constructor
863
 
 
864
 
        :param old_tree: Tree to show as old in the comparison
865
 
        :param new_tree: Tree to show as new in the comparison
866
 
        :param to_file: File to write comparision to
867
 
        :param path_encoding: Character encoding to write paths in
868
 
        :param diff_text: DiffPath-type object to use as a last resort for
869
 
            diffing text files.
870
 
        :param extra_factories: Factories of DiffPaths to try before any other
871
 
            DiffPaths"""
872
 
        if diff_text is None:
873
 
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
874
 
                                 '', '',  internal_diff)
875
 
        self.old_tree = old_tree
876
 
        self.new_tree = new_tree
877
 
        self.to_file = to_file
878
 
        self.path_encoding = path_encoding
879
 
        self.differs = []
880
 
        if extra_factories is not None:
881
 
            self.differs.extend(f(self) for f in extra_factories)
882
 
        self.differs.extend(f(self) for f in self.diff_factories)
883
 
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
884
 
 
885
 
    @classmethod
886
 
    def from_trees_options(klass, old_tree, new_tree, to_file,
887
 
                           path_encoding, external_diff_options, old_label,
888
 
                           new_label, using):
889
 
        """Factory for producing a DiffTree.
890
 
 
891
 
        Designed to accept options used by show_diff_trees.
892
 
        :param old_tree: The tree to show as old in the comparison
893
 
        :param new_tree: The tree to show as new in the comparison
894
 
        :param to_file: File to write comparisons to
895
 
        :param path_encoding: Character encoding to use for writing paths
896
 
        :param external_diff_options: If supplied, use the installed diff
897
 
            binary to perform file comparison, using supplied options.
898
 
        :param old_label: Prefix to use for old file labels
899
 
        :param new_label: Prefix to use for new file labels
900
 
        :param using: Commandline to use to invoke an external diff tool
901
 
        """
902
 
        if using is not None:
903
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using)]
904
 
        else:
905
 
            extra_factories = []
906
 
        if external_diff_options:
907
 
            assert isinstance(external_diff_options, basestring)
908
 
            opts = external_diff_options.split()
909
 
            def diff_file(olab, olines, nlab, nlines, to_file):
910
 
                external_diff(olab, olines, nlab, nlines, to_file, opts)
911
 
        else:
912
 
            diff_file = internal_diff
913
 
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
914
 
                             old_label, new_label, diff_file)
915
 
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
916
 
                     extra_factories)
917
 
 
918
 
    def show_diff(self, specific_files, extra_trees=None):
919
 
        """Write tree diff to self.to_file
920
 
 
921
 
        :param sepecific_files: the specific files to compare (recursive)
922
 
        :param extra_trees: extra trees to use for mapping paths to file_ids
923
 
        """
924
 
        try:
925
 
            return self._show_diff(specific_files, extra_trees)
926
 
        finally:
927
 
            for differ in self.differs:
928
 
                differ.finish()
929
 
 
930
 
    def _show_diff(self, specific_files, extra_trees):
931
 
        # TODO: Generation of pseudo-diffs for added/deleted files could
932
 
        # be usefully made into a much faster special case.
933
 
        iterator = self.new_tree.iter_changes(self.old_tree,
934
 
                                               specific_files=specific_files,
935
 
                                               extra_trees=extra_trees,
936
 
                                               require_versioned=True)
937
 
        has_changes = 0
938
 
        def changes_key(change):
939
 
            old_path, new_path = change[1]
940
 
            path = new_path
941
 
            if path is None:
942
 
                path = old_path
943
 
            return path
944
 
        def get_encoded_path(path):
945
 
            if path is not None:
946
 
                return path.encode(self.path_encoding, "replace")
947
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
948
 
             executable) in sorted(iterator, key=changes_key):
949
 
            if parent == (None, None):
950
 
                continue
951
 
            oldpath, newpath = paths
952
 
            oldpath_encoded = get_encoded_path(paths[0])
953
 
            newpath_encoded = get_encoded_path(paths[1])
954
 
            old_present = (kind[0] is not None and versioned[0])
955
 
            new_present = (kind[1] is not None and versioned[1])
956
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
957
 
            prop_str = get_prop_change(executable[0] != executable[1])
958
 
            if (old_present, new_present) == (True, False):
959
 
                self.to_file.write("=== removed %s '%s'\n" %
960
 
                                   (kind[0], oldpath_encoded))
961
 
                newpath = oldpath
962
 
            elif (old_present, new_present) == (False, True):
963
 
                self.to_file.write("=== added %s '%s'\n" %
964
 
                                   (kind[1], newpath_encoded))
965
 
                oldpath = newpath
966
 
            elif renamed:
967
 
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
968
 
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
969
 
            else:
970
 
                # if it was produced by iter_changes, it must be
971
 
                # modified *somehow*, either content or execute bit.
972
 
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
973
 
                                   newpath_encoded, prop_str))
974
 
            if changed_content:
975
 
                self.diff(file_id, oldpath, newpath)
976
 
                has_changes = 1
977
 
            if renamed:
978
 
                has_changes = 1
979
 
        return has_changes
980
 
 
981
 
    def diff(self, file_id, old_path, new_path):
982
 
        """Perform a diff of a single file
983
 
 
984
 
        :param file_id: file-id of the file
985
 
        :param old_path: The path of the file in the old tree
986
 
        :param new_path: The path of the file in the new tree
987
 
        """
988
 
        try:
989
 
            old_kind = self.old_tree.kind(file_id)
990
 
        except (errors.NoSuchId, errors.NoSuchFile):
991
 
            old_kind = None
992
 
        try:
993
 
            new_kind = self.new_tree.kind(file_id)
994
 
        except (errors.NoSuchId, errors.NoSuchFile):
995
 
            new_kind = None
996
 
 
997
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
998
 
                                       new_path, old_kind, new_kind)
999
 
        if result is DiffPath.CANNOT_DIFF:
1000
 
            error_path = new_path
1001
 
            if error_path is None:
1002
 
                error_path = old_path
1003
 
            raise errors.NoDiffFound(error_path)
 
525
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
 
526
                                new_path, new_tree, text_modified,
 
527
                                kind, to_file, diff_file):
 
528
    if text_modified:
 
529
        new_entry = new_tree.inventory[file_id]
 
530
        old_tree.inventory[file_id].diff(diff_file,
 
531
                                         old_path, old_tree,
 
532
                                         new_path, new_entry, 
 
533
                                         new_tree, to_file)