~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Vincent Ladeuil
  • Date: 2013-07-13 19:05:24 UTC
  • mto: This revision was merged to the branch mainline in revision 6580.
  • Revision ID: v.ladeuil+lp@free.fr-20130713190524-3bclzq4hpwkd6hkw
Urgh. pqm still runs python 2.6 so we have to maintain compatibility to land the fix 8-(

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
 
1
# Copyright (C) 2005-2011 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
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
 
17
from __future__ import absolute_import
 
18
 
17
19
import difflib
18
20
import os
19
21
import re
20
 
import shutil
 
22
import string
21
23
import sys
22
24
 
23
25
from bzrlib.lazy_import import lazy_import
25
27
import errno
26
28
import subprocess
27
29
import tempfile
28
 
import time
29
30
 
30
31
from bzrlib import (
31
 
    branch as _mod_branch,
32
 
    bzrdir,
33
 
    commands,
 
32
    cleanup,
 
33
    cmdline,
 
34
    controldir,
34
35
    errors,
35
36
    osutils,
36
37
    patiencediff,
38
39
    timestamp,
39
40
    views,
40
41
    )
 
42
 
 
43
from bzrlib.workingtree import WorkingTree
 
44
from bzrlib.i18n import gettext
41
45
""")
42
46
 
43
 
from bzrlib.symbol_versioning import (
44
 
    deprecated_function,
 
47
from bzrlib.registry import (
 
48
    Registry,
45
49
    )
46
50
from bzrlib.trace import mutter, note, warning
47
51
 
 
52
DEFAULT_CONTEXT_AMOUNT = 3
 
53
 
 
54
class AtTemplate(string.Template):
 
55
    """Templating class that uses @ instead of $."""
 
56
 
 
57
    delimiter = '@'
 
58
 
48
59
 
49
60
# TODO: Rather than building a changeset object, we should probably
50
61
# invoke callbacks on an object.  That object can either accumulate a
62
73
 
63
74
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
64
75
                  allow_binary=False, sequence_matcher=None,
65
 
                  path_encoding='utf8'):
 
76
                  path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
66
77
    # FIXME: difflib is wrong if there is no trailing newline.
67
78
    # The syntax used by patch seems to be "\ No newline at
68
79
    # end of file" following the last diff line from that
86
97
    if sequence_matcher is None:
87
98
        sequence_matcher = patiencediff.PatienceSequenceMatcher
88
99
    ud = patiencediff.unified_diff(oldlines, newlines,
89
 
                      fromfile=old_filename.encode(path_encoding),
90
 
                      tofile=new_filename.encode(path_encoding),
91
 
                      sequencematcher=sequence_matcher)
 
100
                      fromfile=old_filename.encode(path_encoding, 'replace'),
 
101
                      tofile=new_filename.encode(path_encoding, 'replace'),
 
102
                      n=context_lines, sequencematcher=sequence_matcher)
92
103
 
93
104
    ud = list(ud)
94
105
    if len(ud) == 0: # Identical contents, nothing to do
171
182
 
172
183
        if not diff_opts:
173
184
            diff_opts = []
 
185
        if sys.platform == 'win32':
 
186
            # Popen doesn't do the proper encoding for external commands
 
187
            # Since we are dealing with an ANSI api, use mbcs encoding
 
188
            old_filename = old_filename.encode('mbcs')
 
189
            new_filename = new_filename.encode('mbcs')
174
190
        diffcmd = ['diff',
175
191
                   '--label', old_filename,
176
192
                   old_abspath,
272
288
                        new_abspath, e)
273
289
 
274
290
 
275
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url,
276
 
    apply_view=True):
 
291
def get_trees_and_branches_to_diff_locked(
 
292
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
277
293
    """Get the trees and specific files to diff given a list of paths.
278
294
 
279
295
    This method works out the trees to be diff'ed and the files of
290
306
    :param new_url:
291
307
        The url of the new branch or tree. If None, the tree to use is
292
308
        taken from the first path, if any, or the current working tree.
 
309
    :param add_cleanup:
 
310
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
311
        will register cleanups that must be run to unlock the trees, etc.
293
312
    :param apply_view:
294
313
        if True and a view is set, apply the view or check that the paths
295
314
        are within it
296
315
    :returns:
297
 
        a tuple of (old_tree, new_tree, specific_files, extra_trees) where
298
 
        extra_trees is a sequence of additional trees to search in for
299
 
        file-ids.
 
316
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
317
        specific_files, extra_trees) where extra_trees is a sequence of
 
318
        additional trees to search in for file-ids.  The trees and branches
 
319
        will be read-locked until the cleanups registered via the add_cleanup
 
320
        param are run.
300
321
    """
301
322
    # Get the old and new revision specs
302
323
    old_revision_spec = None
325
346
        default_location = path_list[0]
326
347
        other_paths = path_list[1:]
327
348
 
 
349
    def lock_tree_or_branch(wt, br):
 
350
        if wt is not None:
 
351
            wt.lock_read()
 
352
            add_cleanup(wt.unlock)
 
353
        elif br is not None:
 
354
            br.lock_read()
 
355
            add_cleanup(br.unlock)
 
356
 
328
357
    # Get the old location
329
358
    specific_files = []
330
359
    if old_url is None:
331
360
        old_url = default_location
332
361
    working_tree, branch, relpath = \
333
 
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
 
362
        controldir.ControlDir.open_containing_tree_or_branch(old_url)
 
363
    lock_tree_or_branch(working_tree, branch)
334
364
    if consider_relpath and relpath != '':
335
365
        if working_tree is not None and apply_view:
336
366
            views.check_path_in_view(working_tree, relpath)
337
367
        specific_files.append(relpath)
338
368
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
 
369
    old_branch = branch
339
370
 
340
371
    # Get the new location
341
372
    if new_url is None:
342
373
        new_url = default_location
343
374
    if new_url != old_url:
344
375
        working_tree, branch, relpath = \
345
 
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
 
376
            controldir.ControlDir.open_containing_tree_or_branch(new_url)
 
377
        lock_tree_or_branch(working_tree, branch)
346
378
        if consider_relpath and relpath != '':
347
379
            if working_tree is not None and apply_view:
348
380
                views.check_path_in_view(working_tree, relpath)
349
381
            specific_files.append(relpath)
350
382
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
351
383
        basis_is_default=working_tree is None)
 
384
    new_branch = branch
352
385
 
353
386
    # Get the specific files (all files is None, no files is [])
354
387
    if make_paths_wt_relative and working_tree is not None:
355
 
        try:
356
 
            from bzrlib.builtins import safe_relpath_files
357
 
            other_paths = safe_relpath_files(working_tree, other_paths,
 
388
        other_paths = working_tree.safe_relpath_files(
 
389
            other_paths,
358
390
            apply_view=apply_view)
359
 
        except errors.FileInWrongBranch:
360
 
            raise errors.BzrCommandError("Files are in different branches")
361
391
    specific_files.extend(other_paths)
362
392
    if len(specific_files) == 0:
363
393
        specific_files = None
367
397
            if view_files:
368
398
                specific_files = view_files
369
399
                view_str = views.view_display_str(view_files)
370
 
                note("*** Ignoring files outside view. View is %s" % view_str)
 
400
                note(gettext("*** Ignoring files outside view. View is %s") % view_str)
371
401
 
372
402
    # Get extra trees that ought to be searched for file-ids
373
403
    extra_trees = None
374
404
    if working_tree is not None and working_tree not in (old_tree, new_tree):
375
405
        extra_trees = (working_tree,)
376
 
    return old_tree, new_tree, specific_files, extra_trees
 
406
    return (old_tree, new_tree, old_branch, new_branch,
 
407
            specific_files, extra_trees)
 
408
 
377
409
 
378
410
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
379
411
    if branch is None and tree is not None:
394
426
                    old_label='a/', new_label='b/',
395
427
                    extra_trees=None,
396
428
                    path_encoding='utf8',
397
 
                    using=None):
 
429
                    using=None,
 
430
                    format_cls=None,
 
431
                    context=DEFAULT_CONTEXT_AMOUNT):
398
432
    """Show in text form the changes from one tree to another.
399
433
 
400
 
    to_file
401
 
        The output stream.
402
 
 
403
 
    specific_files
404
 
        Include only changes to these files - None for all changes.
405
 
 
406
 
    external_diff_options
407
 
        If set, use an external GNU diff and pass these options.
408
 
 
409
 
    extra_trees
410
 
        If set, more Trees to use for looking up file ids
411
 
 
412
 
    path_encoding
413
 
        If set, the path will be encoded as specified, otherwise is supposed
414
 
        to be utf8
 
434
    :param to_file: The output stream.
 
435
    :param specific_files: Include only changes to these files - None for all
 
436
        changes.
 
437
    :param external_diff_options: If set, use an external GNU diff and pass 
 
438
        these options.
 
439
    :param extra_trees: If set, more Trees to use for looking up file ids
 
440
    :param path_encoding: If set, the path will be encoded as specified, 
 
441
        otherwise is supposed to be utf8
 
442
    :param format_cls: Formatter class (DiffTree subclass)
415
443
    """
 
444
    if context is None:
 
445
        context = DEFAULT_CONTEXT_AMOUNT
 
446
    if format_cls is None:
 
447
        format_cls = DiffTree
416
448
    old_tree.lock_read()
417
449
    try:
418
450
        if extra_trees is not None:
420
452
                tree.lock_read()
421
453
        new_tree.lock_read()
422
454
        try:
423
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
424
 
                                                 path_encoding,
425
 
                                                 external_diff_options,
426
 
                                                 old_label, new_label, using)
 
455
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
456
                                                   path_encoding,
 
457
                                                   external_diff_options,
 
458
                                                   old_label, new_label, using,
 
459
                                                   context_lines=context)
427
460
            return differ.show_diff(specific_files, extra_trees)
428
461
        finally:
429
462
            new_tree.unlock()
436
469
 
437
470
def _patch_header_date(tree, file_id, path):
438
471
    """Returns a timestamp suitable for use in a patch header."""
439
 
    mtime = tree.get_file_mtime(file_id, path)
 
472
    try:
 
473
        mtime = tree.get_file_mtime(file_id, path)
 
474
    except errors.FileTimestampUnavailable:
 
475
        mtime = 0
440
476
    return timestamp.format_patch_date(mtime)
441
477
 
442
478
 
584
620
    # or removed in a diff.
585
621
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
586
622
 
587
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
588
 
                 old_label='', new_label='', text_differ=internal_diff):
 
623
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8', 
 
624
                 old_label='', new_label='', text_differ=internal_diff, 
 
625
                 context_lines=DEFAULT_CONTEXT_AMOUNT):
589
626
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
590
627
        self.text_differ = text_differ
591
628
        self.old_label = old_label
592
629
        self.new_label = new_label
593
630
        self.path_encoding = path_encoding
 
631
        self.context_lines = context_lines
594
632
 
595
633
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
596
634
        """Compare two files in unified diff format
637
675
        """
638
676
        def _get_text(tree, file_id, path):
639
677
            if file_id is not None:
640
 
                return tree.get_file(file_id, path).readlines()
 
678
                return tree.get_file_lines(file_id, path)
641
679
            else:
642
680
                return []
643
681
        try:
644
682
            from_text = _get_text(self.old_tree, from_file_id, from_path)
645
683
            to_text = _get_text(self.new_tree, to_file_id, to_path)
646
684
            self.text_differ(from_label, from_text, to_label, to_text,
647
 
                             self.to_file)
 
685
                             self.to_file, path_encoding=self.path_encoding,
 
686
                             context_lines=self.context_lines)
648
687
        except errors.BinaryFile:
649
688
            self.to_file.write(
650
689
                  ("Binary files %s and %s differ\n" %
651
 
                  (from_label, to_label)).encode(self.path_encoding))
 
690
                  (from_label, to_label)).encode(self.path_encoding,'replace'))
652
691
        return self.CHANGED
653
692
 
654
693
 
663
702
    @classmethod
664
703
    def from_string(klass, command_string, old_tree, new_tree, to_file,
665
704
                    path_encoding='utf-8'):
666
 
        command_template = commands.shlex_split_unicode(command_string)
667
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
 
705
        command_template = cmdline.split(command_string)
 
706
        if '@' not in command_string:
 
707
            command_template.extend(['@old_path', '@new_path'])
668
708
        return klass(command_template, old_tree, new_tree, to_file,
669
709
                     path_encoding)
670
710
 
671
711
    @classmethod
672
 
    def make_from_diff_tree(klass, command_string):
 
712
    def make_from_diff_tree(klass, command_string, external_diff_options=None):
673
713
        def from_diff_tree(diff_tree):
674
 
            return klass.from_string(command_string, diff_tree.old_tree,
 
714
            full_command_string = [command_string]
 
715
            if external_diff_options is not None:
 
716
                full_command_string += ' ' + external_diff_options
 
717
            return klass.from_string(full_command_string, diff_tree.old_tree,
675
718
                                     diff_tree.new_tree, diff_tree.to_file)
676
719
        return from_diff_tree
677
720
 
678
721
    def _get_command(self, old_path, new_path):
679
722
        my_map = {'old_path': old_path, 'new_path': new_path}
680
 
        return [t % my_map for t in self.command_template]
 
723
        command = [AtTemplate(t).substitute(my_map) for t in
 
724
                   self.command_template]
 
725
        if sys.platform == 'win32': # Popen doesn't accept unicode on win32
 
726
            command_encoded = []
 
727
            for c in command:
 
728
                if isinstance(c, unicode):
 
729
                    command_encoded.append(c.encode('mbcs'))
 
730
                else:
 
731
                    command_encoded.append(c)
 
732
            return command_encoded
 
733
        else:
 
734
            return command
681
735
 
682
736
    def _execute(self, old_path, new_path):
683
737
        command = self._get_command(old_path, new_path)
703
757
                raise
704
758
        return True
705
759
 
706
 
    def _write_file(self, file_id, tree, prefix, relpath):
707
 
        full_path = osutils.pathjoin(self._root, prefix, relpath)
708
 
        if self._try_symlink_root(tree, prefix):
 
760
    @staticmethod
 
761
    def _fenc():
 
762
        """Returns safe encoding for passing file path to diff tool"""
 
763
        if sys.platform == 'win32':
 
764
            return 'mbcs'
 
765
        else:
 
766
            # Don't fallback to 'utf-8' because subprocess may not be able to
 
767
            # handle utf-8 correctly when locale is not utf-8.
 
768
            return sys.getfilesystemencoding() or 'ascii'
 
769
 
 
770
    def _is_safepath(self, path):
 
771
        """Return true if `path` may be able to pass to subprocess."""
 
772
        fenc = self._fenc()
 
773
        try:
 
774
            return path == path.encode(fenc).decode(fenc)
 
775
        except UnicodeError:
 
776
            return False
 
777
 
 
778
    def _safe_filename(self, prefix, relpath):
 
779
        """Replace unsafe character in `relpath` then join `self._root`,
 
780
        `prefix` and `relpath`."""
 
781
        fenc = self._fenc()
 
782
        # encoded_str.replace('?', '_') may break multibyte char.
 
783
        # So we should encode, decode, then replace(u'?', u'_')
 
784
        relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
 
785
        relpath_tmp = relpath_tmp.replace(u'?', u'_')
 
786
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
 
787
 
 
788
    def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
 
789
                    allow_write=False):
 
790
        if not force_temp and isinstance(tree, WorkingTree):
 
791
            full_path = tree.abspath(tree.id2path(file_id))
 
792
            if self._is_safepath(full_path):
 
793
                return full_path
 
794
 
 
795
        full_path = self._safe_filename(prefix, relpath)
 
796
        if not force_temp and self._try_symlink_root(tree, prefix):
709
797
            return full_path
710
798
        parent_dir = osutils.dirname(full_path)
711
799
        try:
722
810
                target.close()
723
811
        finally:
724
812
            source.close()
725
 
        osutils.make_readonly(full_path)
726
 
        mtime = tree.get_file_mtime(file_id)
727
 
        os.utime(full_path, (mtime, mtime))
 
813
        try:
 
814
            mtime = tree.get_file_mtime(file_id)
 
815
        except errors.FileTimestampUnavailable:
 
816
            pass
 
817
        else:
 
818
            os.utime(full_path, (mtime, mtime))
 
819
        if not allow_write:
 
820
            osutils.make_readonly(full_path)
728
821
        return full_path
729
822
 
730
 
    def _prepare_files(self, file_id, old_path, new_path):
 
823
    def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
 
824
                       allow_write_new=False):
731
825
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
732
 
                                         old_path)
 
826
                                         old_path, force_temp)
733
827
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
734
 
                                         new_path)
 
828
                                         new_path, force_temp,
 
829
                                         allow_write=allow_write_new)
735
830
        return old_disk_path, new_disk_path
736
831
 
737
832
    def finish(self):
745
840
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
746
841
        if (old_kind, new_kind) != ('file', 'file'):
747
842
            return DiffPath.CANNOT_DIFF
748
 
        self._prepare_files(file_id, old_path, new_path)
749
 
        self._execute(osutils.pathjoin('old', old_path),
750
 
                      osutils.pathjoin('new', new_path))
 
843
        (old_disk_path, new_disk_path) = self._prepare_files(
 
844
                                                file_id, old_path, new_path)
 
845
        self._execute(old_disk_path, new_disk_path)
 
846
 
 
847
    def edit_file(self, file_id):
 
848
        """Use this tool to edit a file.
 
849
 
 
850
        A temporary copy will be edited, and the new contents will be
 
851
        returned.
 
852
 
 
853
        :param file_id: The id of the file to edit.
 
854
        :return: The new contents of the file.
 
855
        """
 
856
        old_path = self.old_tree.id2path(file_id)
 
857
        new_path = self.new_tree.id2path(file_id)
 
858
        old_abs_path, new_abs_path = self._prepare_files(
 
859
                                            file_id, old_path, new_path,
 
860
                                            allow_write_new=True,
 
861
                                            force_temp=True)
 
862
        command = self._get_command(old_abs_path, new_abs_path)
 
863
        subprocess.call(command, cwd=self._root)
 
864
        new_file = open(new_abs_path, 'rb')
 
865
        try:
 
866
            return new_file.read()
 
867
        finally:
 
868
            new_file.close()
751
869
 
752
870
 
753
871
class DiffTree(object):
795
913
    @classmethod
796
914
    def from_trees_options(klass, old_tree, new_tree, to_file,
797
915
                           path_encoding, external_diff_options, old_label,
798
 
                           new_label, using):
 
916
                           new_label, using, context_lines):
799
917
        """Factory for producing a DiffTree.
800
918
 
801
919
        Designed to accept options used by show_diff_trees.
 
920
 
802
921
        :param old_tree: The tree to show as old in the comparison
803
922
        :param new_tree: The tree to show as new in the comparison
804
923
        :param to_file: File to write comparisons to
810
929
        :param using: Commandline to use to invoke an external diff tool
811
930
        """
812
931
        if using is not None:
813
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using)]
 
932
            extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
814
933
        else:
815
934
            extra_factories = []
816
935
        if external_diff_options:
817
936
            opts = external_diff_options.split()
818
 
            def diff_file(olab, olines, nlab, nlines, to_file):
 
937
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
 
938
                """:param path_encoding: not used but required
 
939
                        to match the signature of internal_diff.
 
940
                """
819
941
                external_diff(olab, olines, nlab, nlines, to_file, opts)
820
942
        else:
821
943
            diff_file = internal_diff
822
944
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
823
 
                             old_label, new_label, diff_file)
 
945
                             old_label, new_label, diff_file, context_lines=context_lines)
824
946
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
825
947
                     extra_factories)
826
948
 
827
949
    def show_diff(self, specific_files, extra_trees=None):
828
950
        """Write tree diff to self.to_file
829
951
 
830
 
        :param sepecific_files: the specific files to compare (recursive)
 
952
        :param specific_files: the specific files to compare (recursive)
831
953
        :param extra_trees: extra trees to use for mapping paths to file_ids
832
954
        """
833
955
        try:
923
1045
            if error_path is None:
924
1046
                error_path = old_path
925
1047
            raise errors.NoDiffFound(error_path)
 
1048
 
 
1049
 
 
1050
format_registry = Registry()
 
1051
format_registry.register('default', DiffTree)