~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Robert Collins
  • Date: 2010-04-08 04:34:03 UTC
  • mfrom: (5138 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5139.
  • Revision ID: robertc@robertcollins.net-20100408043403-56z0d07vdqrx7f3t
Update bugfix for 528114 to trunk.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
 
1
# Copyright (C) 2005-2010 Canonical Ltd.
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
import difflib
18
18
import os
19
19
import re
20
20
import shutil
 
21
import string
21
22
import sys
22
23
 
23
24
from bzrlib.lazy_import import lazy_import
28
29
import time
29
30
 
30
31
from bzrlib import (
 
32
    branch as _mod_branch,
31
33
    bzrdir,
32
 
    commands,
 
34
    cmdline,
33
35
    errors,
34
36
    osutils,
35
37
    patiencediff,
36
38
    textfile,
37
39
    timestamp,
 
40
    views,
38
41
    )
 
42
 
 
43
from bzrlib.workingtree import WorkingTree
39
44
""")
40
45
 
 
46
from bzrlib.registry import (
 
47
    Registry,
 
48
    )
41
49
from bzrlib.symbol_versioning import (
42
 
        deprecated_function,
43
 
        one_zero,
44
 
        )
45
 
from bzrlib.trace import mutter, warning
 
50
    deprecated_function,
 
51
    )
 
52
from bzrlib.trace import mutter, note, warning
 
53
 
 
54
 
 
55
class AtTemplate(string.Template):
 
56
    """Templating class that uses @ instead of $."""
 
57
 
 
58
    delimiter = '@'
46
59
 
47
60
 
48
61
# TODO: Rather than building a changeset object, we should probably
77
90
    # both sequences are empty.
78
91
    if not oldlines and not newlines:
79
92
        return
80
 
    
 
93
 
81
94
    if allow_binary is False:
82
95
        textfile.check_text_lines(oldlines)
83
96
        textfile.check_text_lines(newlines)
98
111
        ud[2] = ud[2].replace('-1,0', '-0,0')
99
112
    elif not newlines:
100
113
        ud[2] = ud[2].replace('+1,0', '+0,0')
101
 
    # work around for difflib emitting random spaces after the label
102
 
    ud[0] = ud[0][:-2] + '\n'
103
 
    ud[1] = ud[1][:-2] + '\n'
104
114
 
105
115
    for line in ud:
106
116
        to_file.write(line)
173
183
 
174
184
        if not diff_opts:
175
185
            diff_opts = []
 
186
        if sys.platform == 'win32':
 
187
            # Popen doesn't do the proper encoding for external commands
 
188
            # Since we are dealing with an ANSI api, use mbcs encoding
 
189
            old_filename = old_filename.encode('mbcs')
 
190
            new_filename = new_filename.encode('mbcs')
176
191
        diffcmd = ['diff',
177
192
                   '--label', old_filename,
178
193
                   old_abspath,
201
216
            break
202
217
        else:
203
218
            diffcmd.append('-u')
204
 
                  
 
219
 
205
220
        if diff_opts:
206
221
            diffcmd.extend(diff_opts)
207
222
 
208
223
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
209
224
        out,err = pipe.communicate()
210
225
        rc = pipe.returncode
211
 
        
 
226
 
212
227
        # internal_diff() adds a trailing newline, add one here for consistency
213
228
        out += '\n'
214
229
        if rc == 2:
249
264
                msg = 'signal %d' % (-rc)
250
265
            else:
251
266
                msg = 'exit code %d' % rc
252
 
                
253
 
            raise errors.BzrError('external diff failed with %s; command: %r' 
 
267
 
 
268
            raise errors.BzrError('external diff failed with %s; command: %r'
254
269
                                  % (rc, diffcmd))
255
270
 
256
271
 
274
289
                        new_abspath, e)
275
290
 
276
291
 
277
 
@deprecated_function(one_zero)
278
 
def diff_cmd_helper(tree, specific_files, external_diff_options, 
279
 
                    old_revision_spec=None, new_revision_spec=None,
280
 
                    revision_specs=None,
281
 
                    old_label='a/', new_label='b/'):
282
 
    """Helper for cmd_diff.
283
 
 
284
 
    :param tree:
285
 
        A WorkingTree
286
 
 
287
 
    :param specific_files:
288
 
        The specific files to compare, or None
289
 
 
290
 
    :param external_diff_options:
291
 
        If non-None, run an external diff, and pass it these options
292
 
 
293
 
    :param old_revision_spec:
294
 
        If None, use basis tree as old revision, otherwise use the tree for
295
 
        the specified revision. 
296
 
 
297
 
    :param new_revision_spec:
298
 
        If None, use working tree as new revision, otherwise use the tree for
299
 
        the specified revision.
300
 
    
301
 
    :param revision_specs: 
302
 
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
303
 
        to compare.  This can be passed as an alternative to the old_revision_spec 
304
 
        and new_revision_spec parameters.
305
 
 
306
 
    The more general form is show_diff_trees(), where the caller
307
 
    supplies any two trees.
308
 
    """
309
 
 
310
 
    # TODO: perhaps remove the old parameters old_revision_spec and
311
 
    # new_revision_spec, since this is only really for use from cmd_diff and
312
 
    # it now always passes through a sequence of revision_specs -- mbp
313
 
    # 20061221
314
 
 
315
 
    def spec_tree(spec):
316
 
        if tree:
317
 
            revision = spec.in_store(tree.branch)
318
 
        else:
319
 
            revision = spec.in_store(None)
320
 
        revision_id = revision.rev_id
321
 
        branch = revision.branch
322
 
        return branch.repository.revision_tree(revision_id)
323
 
 
324
 
    if revision_specs is not None:
325
 
        assert (old_revision_spec is None
326
 
                and new_revision_spec is None)
327
 
        if len(revision_specs) > 0:
328
 
            old_revision_spec = revision_specs[0]
329
 
        if len(revision_specs) > 1:
330
 
            new_revision_spec = revision_specs[1]
331
 
 
332
 
    if old_revision_spec is None:
333
 
        old_tree = tree.basis_tree()
334
 
    else:
335
 
        old_tree = spec_tree(old_revision_spec)
336
 
 
337
 
    if (new_revision_spec is None
338
 
        or new_revision_spec.spec is None):
339
 
        new_tree = tree
340
 
    else:
341
 
        new_tree = spec_tree(new_revision_spec)
342
 
 
343
 
    if new_tree is not tree:
344
 
        extra_trees = (tree,)
345
 
    else:
346
 
        extra_trees = None
347
 
 
348
 
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
349
 
                           external_diff_options,
350
 
                           old_label=old_label, new_label=new_label,
351
 
                           extra_trees=extra_trees)
352
 
 
353
 
 
354
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
 
292
def get_trees_and_branches_to_diff(path_list, revision_specs, old_url, new_url,
 
293
                                   apply_view=True):
355
294
    """Get the trees and specific files to diff given a list of paths.
356
295
 
357
296
    This method works out the trees to be diff'ed and the files of
368
307
    :param new_url:
369
308
        The url of the new branch or tree. If None, the tree to use is
370
309
        taken from the first path, if any, or the current working tree.
 
310
    :param apply_view:
 
311
        if True and a view is set, apply the view or check that the paths
 
312
        are within it
371
313
    :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.
 
314
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
315
        specific_files, extra_trees) where extra_trees is a sequence of
 
316
        additional trees to search in for file-ids.
375
317
    """
376
318
    # Get the old and new revision specs
377
319
    old_revision_spec = None
388
330
 
389
331
    other_paths = []
390
332
    make_paths_wt_relative = True
 
333
    consider_relpath = True
391
334
    if path_list is None or len(path_list) == 0:
392
 
        # If no path is given, assume the current directory
 
335
        # If no path is given, the current working tree is used
393
336
        default_location = u'.'
 
337
        consider_relpath = False
394
338
    elif old_url is not None and new_url is not None:
395
339
        other_paths = path_list
396
340
        make_paths_wt_relative = False
404
348
        old_url = default_location
405
349
    working_tree, branch, relpath = \
406
350
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
407
 
    if relpath != '':
 
351
    if consider_relpath and relpath != '':
 
352
        if working_tree is not None and apply_view:
 
353
            views.check_path_in_view(working_tree, relpath)
408
354
        specific_files.append(relpath)
409
355
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
 
356
    old_branch = branch
410
357
 
411
358
    # Get the new location
412
359
    if new_url is None:
414
361
    if new_url != old_url:
415
362
        working_tree, branch, relpath = \
416
363
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
417
 
        if relpath != '':
 
364
        if consider_relpath and relpath != '':
 
365
            if working_tree is not None and apply_view:
 
366
                views.check_path_in_view(working_tree, relpath)
418
367
            specific_files.append(relpath)
419
368
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
420
369
        basis_is_default=working_tree is None)
 
370
    new_branch = branch
421
371
 
422
372
    # Get the specific files (all files is None, no files is [])
423
373
    if make_paths_wt_relative and working_tree is not None:
424
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
 
374
        try:
 
375
            from bzrlib.builtins import safe_relpath_files
 
376
            other_paths = safe_relpath_files(working_tree, other_paths,
 
377
            apply_view=apply_view)
 
378
        except errors.FileInWrongBranch:
 
379
            raise errors.BzrCommandError("Files are in different branches")
425
380
    specific_files.extend(other_paths)
426
381
    if len(specific_files) == 0:
427
382
        specific_files = None
 
383
        if (working_tree is not None and working_tree.supports_views()
 
384
            and apply_view):
 
385
            view_files = working_tree.views.lookup_view()
 
386
            if view_files:
 
387
                specific_files = view_files
 
388
                view_str = views.view_display_str(view_files)
 
389
                note("*** Ignoring files outside view. View is %s" % view_str)
428
390
 
429
391
    # Get extra trees that ought to be searched for file-ids
430
392
    extra_trees = None
431
393
    if working_tree is not None and working_tree not in (old_tree, new_tree):
432
394
        extra_trees = (working_tree,)
433
 
    return old_tree, new_tree, specific_files, extra_trees
 
395
    return old_tree, new_tree, old_branch, new_branch, specific_files, extra_trees
434
396
 
435
397
 
436
398
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
444
406
                return branch.basis_tree()
445
407
        else:
446
408
            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
 
409
    return spec.as_tree(branch)
466
410
 
467
411
 
468
412
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
470
414
                    old_label='a/', new_label='b/',
471
415
                    extra_trees=None,
472
416
                    path_encoding='utf8',
473
 
                    using=None):
 
417
                    using=None,
 
418
                    format_cls=None):
474
419
    """Show in text form the changes from one tree to another.
475
420
 
476
 
    to_file
477
 
        The output stream.
478
 
 
479
 
    specific_files
480
 
        Include only changes to these files - None for all changes.
481
 
 
482
 
    external_diff_options
483
 
        If set, use an external GNU diff and pass these options.
484
 
 
485
 
    extra_trees
486
 
        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
 
421
    :param to_file: The output stream.
 
422
    :param specific_files:Include only changes to these files - None for all
 
423
        changes.
 
424
    :param external_diff_options: If set, use an external GNU diff and pass 
 
425
        these options.
 
426
    :param extra_trees: If set, more Trees to use for looking up file ids
 
427
    :param path_encoding: If set, the path will be encoded as specified, 
 
428
        otherwise is supposed to be utf8
 
429
    :param format_cls: Formatter class (DiffTree subclass)
491
430
    """
 
431
    if format_cls is None:
 
432
        format_cls = DiffTree
492
433
    old_tree.lock_read()
493
434
    try:
494
435
        if extra_trees is not None:
496
437
                tree.lock_read()
497
438
        new_tree.lock_read()
498
439
        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)
 
440
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
441
                                                   path_encoding,
 
442
                                                   external_diff_options,
 
443
                                                   old_label, new_label, using)
503
444
            return differ.show_diff(specific_files, extra_trees)
504
445
        finally:
505
446
            new_tree.unlock()
512
453
 
513
454
def _patch_header_date(tree, file_id, path):
514
455
    """Returns a timestamp suitable for use in a patch header."""
515
 
    mtime = tree.get_file_mtime(file_id, path)
516
 
    assert mtime is not None, \
517
 
        "got an mtime of None for file-id %s, path %s in tree %s" % (
518
 
                file_id, path, tree)
 
456
    try:
 
457
        mtime = tree.get_file_mtime(file_id, path)
 
458
    except errors.FileTimestampUnavailable:
 
459
        mtime = 0
519
460
    return timestamp.format_patch_date(mtime)
520
461
 
521
462
 
522
 
def _raise_if_nonexistent(paths, old_tree, new_tree):
523
 
    """Complain if paths are not in either inventory or tree.
524
 
 
525
 
    It's OK with the files exist in either tree's inventory, or 
526
 
    if they exist in the tree but are not versioned.
527
 
    
528
 
    This can be used by operations such as bzr status that can accept
529
 
    unknown or ignored files.
530
 
    """
531
 
    mutter("check paths: %r", paths)
532
 
    if not paths:
533
 
        return
534
 
    s = old_tree.filter_unversioned_files(paths)
535
 
    s = new_tree.filter_unversioned_files(s)
536
 
    s = [path for path in s if not new_tree.has_filename(path)]
537
 
    if s:
538
 
        raise errors.PathsDoNotExist(sorted(s))
539
 
 
540
 
 
541
 
def get_prop_change(meta_modified):
542
 
    if meta_modified:
543
 
        return " (properties changed)"
 
463
def get_executable_change(old_is_x, new_is_x):
 
464
    descr = { True:"+x", False:"-x", None:"??" }
 
465
    if old_is_x != new_is_x:
 
466
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
544
467
    else:
545
 
        return  ""
 
468
        return []
546
469
 
547
470
 
548
471
class DiffPath(object):
717
640
            return self.CANNOT_DIFF
718
641
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
719
642
        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)
 
643
        return self.diff_text(from_file_id, to_file_id, from_label, to_label,
 
644
            old_path, new_path)
721
645
 
722
 
    def diff_text(self, from_file_id, to_file_id, from_label, to_label):
 
646
    def diff_text(self, from_file_id, to_file_id, from_label, to_label,
 
647
        from_path=None, to_path=None):
723
648
        """Diff the content of given files in two trees
724
649
 
725
650
        :param from_file_id: The id of the file in the from tree.  If None,
727
652
        :param to_file_id: The id of the file in the to tree.  This may refer
728
653
            to a different file from from_file_id.  If None,
729
654
            the file is not present in the to tree.
 
655
        :param from_path: The path in the from tree or None if unknown.
 
656
        :param to_path: The path in the to tree or None if unknown.
730
657
        """
731
 
        def _get_text(tree, file_id):
 
658
        def _get_text(tree, file_id, path):
732
659
            if file_id is not None:
733
 
                return tree.get_file(file_id).readlines()
 
660
                return tree.get_file(file_id, path).readlines()
734
661
            else:
735
662
                return []
736
663
        try:
737
 
            from_text = _get_text(self.old_tree, from_file_id)
738
 
            to_text = _get_text(self.new_tree, to_file_id)
 
664
            from_text = _get_text(self.old_tree, from_file_id, from_path)
 
665
            to_text = _get_text(self.new_tree, to_file_id, to_path)
739
666
            self.text_differ(from_label, from_text, to_label, to_text,
740
667
                             self.to_file)
741
668
        except errors.BinaryFile:
751
678
                 path_encoding='utf-8'):
752
679
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
753
680
        self.command_template = command_template
754
 
        self._root = tempfile.mkdtemp(prefix='bzr-diff-')
 
681
        self._root = osutils.mkdtemp(prefix='bzr-diff-')
755
682
 
756
683
    @classmethod
757
684
    def from_string(klass, command_string, old_tree, new_tree, to_file,
758
685
                    path_encoding='utf-8'):
759
 
        command_template = commands.shlex_split_unicode(command_string)
760
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
 
686
        command_template = cmdline.split(command_string)
 
687
        if '@' not in command_string:
 
688
            command_template.extend(['@old_path', '@new_path'])
761
689
        return klass(command_template, old_tree, new_tree, to_file,
762
690
                     path_encoding)
763
691
 
770
698
 
771
699
    def _get_command(self, old_path, new_path):
772
700
        my_map = {'old_path': old_path, 'new_path': new_path}
773
 
        return [t % my_map for t in self.command_template]
 
701
        return [AtTemplate(t).substitute(my_map) for t in
 
702
                self.command_template]
774
703
 
775
704
    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)
 
705
        command = self._get_command(old_path, new_path)
 
706
        try:
 
707
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
 
708
                                    cwd=self._root)
 
709
        except OSError, e:
 
710
            if e.errno == errno.ENOENT:
 
711
                raise errors.ExecutableMissing(command[0])
 
712
            else:
 
713
                raise
778
714
        self.to_file.write(proc.stdout.read())
779
715
        return proc.wait()
780
716
 
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)
 
717
    def _try_symlink_root(self, tree, prefix):
 
718
        if (getattr(tree, 'abspath', None) is None
 
719
            or not osutils.host_os_dereferences_symlinks()):
 
720
            return False
 
721
        try:
 
722
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
 
723
        except OSError, e:
 
724
            if e.errno != errno.EEXIST:
 
725
                raise
 
726
        return True
 
727
 
 
728
    def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
 
729
                    allow_write=False):
 
730
        if not force_temp and isinstance(tree, WorkingTree):
 
731
            return tree.abspath(tree.id2path(file_id))
 
732
        
 
733
        full_path = osutils.pathjoin(self._root, prefix, relpath)
 
734
        if not force_temp and self._try_symlink_root(tree, prefix):
 
735
            return full_path
 
736
        parent_dir = osutils.dirname(full_path)
784
737
        try:
785
738
            os.makedirs(parent_dir)
786
739
        except OSError, e:
787
740
            if e.errno != errno.EEXIST:
788
741
                raise
789
 
        source = tree.get_file(file_id)
 
742
        source = tree.get_file(file_id, relpath)
790
743
        try:
791
 
            target = open(full_old_path, 'wb')
 
744
            target = open(full_path, 'wb')
792
745
            try:
793
746
                osutils.pumpfile(source, target)
794
747
            finally:
795
748
                target.close()
796
749
        finally:
797
750
            source.close()
798
 
        return full_old_path
 
751
        if not allow_write:
 
752
            osutils.make_readonly(full_path)
 
753
        try:
 
754
            mtime = tree.get_file_mtime(file_id)
 
755
        except errors.FileTimestampUnavailable:
 
756
            mtime = 0
 
757
        os.utime(full_path, (mtime, mtime))
 
758
        return full_path
799
759
 
800
 
    def _prepare_files(self, file_id, old_path, new_path):
 
760
    def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
 
761
                       allow_write_new=False):
801
762
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
802
 
                                         old_path)
 
763
                                         old_path, force_temp)
803
764
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
804
 
                                         new_path)
 
765
                                         new_path, force_temp,
 
766
                                         allow_write=allow_write_new)
805
767
        return old_disk_path, new_disk_path
806
768
 
807
769
    def finish(self):
808
 
        shutil.rmtree(self._root)
 
770
        try:
 
771
            osutils.rmtree(self._root)
 
772
        except OSError, e:
 
773
            if e.errno != errno.ENOENT:
 
774
                mutter("The temporary directory \"%s\" was not "
 
775
                        "cleanly removed: %s." % (self._root, e))
809
776
 
810
777
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
811
778
        if (old_kind, new_kind) != ('file', 'file'):
812
779
            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))
 
780
        (old_disk_path, new_disk_path) = self._prepare_files(
 
781
                                                file_id, old_path, new_path)
 
782
        self._execute(old_disk_path, new_disk_path)
 
783
 
 
784
    def edit_file(self, file_id):
 
785
        """Use this tool to edit a file.
 
786
 
 
787
        A temporary copy will be edited, and the new contents will be
 
788
        returned.
 
789
 
 
790
        :param file_id: The id of the file to edit.
 
791
        :return: The new contents of the file.
 
792
        """
 
793
        old_path = self.old_tree.id2path(file_id)
 
794
        new_path = self.new_tree.id2path(file_id)
 
795
        new_abs_path = self._prepare_files(file_id, old_path, new_path,
 
796
                                           allow_write_new=True,
 
797
                                           force_temp=True)[1]
 
798
        command = self._get_command(osutils.pathjoin('old', old_path),
 
799
                                    osutils.pathjoin('new', new_path))
 
800
        subprocess.call(command, cwd=self._root)
 
801
        new_file = open(new_abs_path, 'r')
 
802
        try:
 
803
            return new_file.read()
 
804
        finally:
 
805
            new_file.close()
816
806
 
817
807
 
818
808
class DiffTree(object):
879
869
        else:
880
870
            extra_factories = []
881
871
        if external_diff_options:
882
 
            assert isinstance(external_diff_options, basestring)
883
872
            opts = external_diff_options.split()
884
873
            def diff_file(olab, olines, nlab, nlines, to_file):
885
874
                external_diff(olab, olines, nlab, nlines, to_file, opts)
893
882
    def show_diff(self, specific_files, extra_trees=None):
894
883
        """Write tree diff to self.to_file
895
884
 
896
 
        :param sepecific_files: the specific files to compare (recursive)
 
885
        :param specific_files: the specific files to compare (recursive)
897
886
        :param extra_trees: extra trees to use for mapping paths to file_ids
898
887
        """
899
888
        try:
905
894
    def _show_diff(self, specific_files, extra_trees):
906
895
        # TODO: Generation of pseudo-diffs for added/deleted files could
907
896
        # be usefully made into a much faster special case.
908
 
        iterator = self.new_tree._iter_changes(self.old_tree,
 
897
        iterator = self.new_tree.iter_changes(self.old_tree,
909
898
                                               specific_files=specific_files,
910
899
                                               extra_trees=extra_trees,
911
900
                                               require_versioned=True)
921
910
                return path.encode(self.path_encoding, "replace")
922
911
        for (file_id, paths, changed_content, versioned, parent, name, kind,
923
912
             executable) in sorted(iterator, key=changes_key):
924
 
            if parent == (None, None):
 
913
            # The root does not get diffed, and items with no known kind (that
 
914
            # is, missing) in both trees are skipped as well.
 
915
            if parent == (None, None) or kind == (None, None):
925
916
                continue
926
917
            oldpath, newpath = paths
927
918
            oldpath_encoded = get_encoded_path(paths[0])
929
920
            old_present = (kind[0] is not None and versioned[0])
930
921
            new_present = (kind[1] is not None and versioned[1])
931
922
            renamed = (parent[0], name[0]) != (parent[1], name[1])
932
 
            prop_str = get_prop_change(executable[0] != executable[1])
 
923
 
 
924
            properties_changed = []
 
925
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
 
926
 
 
927
            if properties_changed:
 
928
                prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
 
929
            else:
 
930
                prop_str = ""
 
931
 
933
932
            if (old_present, new_present) == (True, False):
934
933
                self.to_file.write("=== removed %s '%s'\n" %
935
934
                                   (kind[0], oldpath_encoded))
942
941
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
943
942
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
944
943
            else:
945
 
                # if it was produced by _iter_changes, it must be
 
944
                # if it was produced by iter_changes, it must be
946
945
                # modified *somehow*, either content or execute bit.
947
946
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
948
947
                                   newpath_encoded, prop_str))
949
948
            if changed_content:
950
 
                self.diff(file_id, oldpath, newpath)
 
949
                self._diff(file_id, oldpath, newpath, kind[0], kind[1])
951
950
                has_changes = 1
952
951
            if renamed:
953
952
                has_changes = 1
968
967
            new_kind = self.new_tree.kind(file_id)
969
968
        except (errors.NoSuchId, errors.NoSuchFile):
970
969
            new_kind = None
971
 
 
 
970
        self._diff(file_id, old_path, new_path, old_kind, new_kind)
 
971
 
 
972
 
 
973
    def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
972
974
        result = DiffPath._diff_many(self.differs, file_id, old_path,
973
975
                                       new_path, old_kind, new_kind)
974
976
        if result is DiffPath.CANNOT_DIFF:
976
978
            if error_path is None:
977
979
                error_path = old_path
978
980
            raise errors.NoDiffFound(error_path)
 
981
 
 
982
 
 
983
format_registry = Registry()
 
984
format_registry.register('default', DiffTree)