~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Martin Pool
  • Date: 2011-06-28 22:25:28 UTC
  • mto: This revision was merged to the branch mainline in revision 6004.
  • Revision ID: mbp@canonical.com-20110628222528-gwf27vdagmxatljc
More explicit laziness

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 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
17
17
import difflib
18
18
import os
19
19
import re
20
 
import shutil
21
20
import string
22
21
import sys
23
22
 
26
25
import errno
27
26
import subprocess
28
27
import tempfile
29
 
import time
30
28
 
31
29
from bzrlib import (
32
 
    branch as _mod_branch,
33
30
    bzrdir,
34
31
    cmdline,
 
32
    cleanup,
35
33
    errors,
36
34
    osutils,
37
35
    patiencediff,
43
41
from bzrlib.workingtree import WorkingTree
44
42
""")
45
43
 
 
44
from bzrlib.registry import (
 
45
    Registry,
 
46
    )
46
47
from bzrlib.symbol_versioning import (
47
48
    deprecated_function,
 
49
    deprecated_in,
48
50
    )
49
51
from bzrlib.trace import mutter, note, warning
50
52
 
95
97
    if sequence_matcher is None:
96
98
        sequence_matcher = patiencediff.PatienceSequenceMatcher
97
99
    ud = patiencediff.unified_diff(oldlines, newlines,
98
 
                      fromfile=old_filename.encode(path_encoding),
99
 
                      tofile=new_filename.encode(path_encoding),
 
100
                      fromfile=old_filename.encode(path_encoding, 'replace'),
 
101
                      tofile=new_filename.encode(path_encoding, 'replace'),
100
102
                      sequencematcher=sequence_matcher)
101
103
 
102
104
    ud = list(ud)
286
288
                        new_abspath, e)
287
289
 
288
290
 
 
291
@deprecated_function(deprecated_in((2, 2, 0)))
289
292
def get_trees_and_branches_to_diff(path_list, revision_specs, old_url, new_url,
290
293
                                   apply_view=True):
291
294
    """Get the trees and specific files to diff given a list of paths.
310
313
    :returns:
311
314
        a tuple of (old_tree, new_tree, old_branch, new_branch,
312
315
        specific_files, extra_trees) where extra_trees is a sequence of
313
 
        additional trees to search in for file-ids.
 
316
        additional trees to search in for file-ids.  The trees and branches
 
317
        are not locked.
 
318
    """
 
319
    op = cleanup.OperationWithCleanups(get_trees_and_branches_to_diff_locked)
 
320
    return op.run_simple(path_list, revision_specs, old_url, new_url,
 
321
            op.add_cleanup, apply_view=apply_view)
 
322
    
 
323
 
 
324
def get_trees_and_branches_to_diff_locked(
 
325
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
326
    """Get the trees and specific files to diff given a list of paths.
 
327
 
 
328
    This method works out the trees to be diff'ed and the files of
 
329
    interest within those trees.
 
330
 
 
331
    :param path_list:
 
332
        the list of arguments passed to the diff command
 
333
    :param revision_specs:
 
334
        Zero, one or two RevisionSpecs from the diff command line,
 
335
        saying what revisions to compare.
 
336
    :param old_url:
 
337
        The url of the old branch or tree. If None, the tree to use is
 
338
        taken from the first path, if any, or the current working tree.
 
339
    :param new_url:
 
340
        The url of the new branch or tree. If None, the tree to use is
 
341
        taken from the first path, if any, or the current working tree.
 
342
    :param add_cleanup:
 
343
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
344
        will register cleanups that must be run to unlock the trees, etc.
 
345
    :param apply_view:
 
346
        if True and a view is set, apply the view or check that the paths
 
347
        are within it
 
348
    :returns:
 
349
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
350
        specific_files, extra_trees) where extra_trees is a sequence of
 
351
        additional trees to search in for file-ids.  The trees and branches
 
352
        will be read-locked until the cleanups registered via the add_cleanup
 
353
        param are run.
314
354
    """
315
355
    # Get the old and new revision specs
316
356
    old_revision_spec = None
339
379
        default_location = path_list[0]
340
380
        other_paths = path_list[1:]
341
381
 
 
382
    def lock_tree_or_branch(wt, br):
 
383
        if wt is not None:
 
384
            wt.lock_read()
 
385
            add_cleanup(wt.unlock)
 
386
        elif br is not None:
 
387
            br.lock_read()
 
388
            add_cleanup(br.unlock)
 
389
 
342
390
    # Get the old location
343
391
    specific_files = []
344
392
    if old_url is None:
345
393
        old_url = default_location
346
394
    working_tree, branch, relpath = \
347
395
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
 
396
    lock_tree_or_branch(working_tree, branch)
348
397
    if consider_relpath and relpath != '':
349
398
        if working_tree is not None and apply_view:
350
399
            views.check_path_in_view(working_tree, relpath)
358
407
    if new_url != old_url:
359
408
        working_tree, branch, relpath = \
360
409
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
 
410
        lock_tree_or_branch(working_tree, branch)
361
411
        if consider_relpath and relpath != '':
362
412
            if working_tree is not None and apply_view:
363
413
                views.check_path_in_view(working_tree, relpath)
368
418
 
369
419
    # Get the specific files (all files is None, no files is [])
370
420
    if make_paths_wt_relative and working_tree is not None:
371
 
        try:
372
 
            from bzrlib.builtins import safe_relpath_files
373
 
            other_paths = safe_relpath_files(working_tree, other_paths,
 
421
        other_paths = working_tree.safe_relpath_files(
 
422
            other_paths,
374
423
            apply_view=apply_view)
375
 
        except errors.FileInWrongBranch:
376
 
            raise errors.BzrCommandError("Files are in different branches")
377
424
    specific_files.extend(other_paths)
378
425
    if len(specific_files) == 0:
379
426
        specific_files = None
411
458
                    old_label='a/', new_label='b/',
412
459
                    extra_trees=None,
413
460
                    path_encoding='utf8',
414
 
                    using=None):
 
461
                    using=None,
 
462
                    format_cls=None):
415
463
    """Show in text form the changes from one tree to another.
416
464
 
417
 
    to_file
418
 
        The output stream.
419
 
 
420
 
    specific_files
421
 
        Include only changes to these files - None for all changes.
422
 
 
423
 
    external_diff_options
424
 
        If set, use an external GNU diff and pass these options.
425
 
 
426
 
    extra_trees
427
 
        If set, more Trees to use for looking up file ids
428
 
 
429
 
    path_encoding
430
 
        If set, the path will be encoded as specified, otherwise is supposed
431
 
        to be utf8
 
465
    :param to_file: The output stream.
 
466
    :param specific_files: Include only changes to these files - None for all
 
467
        changes.
 
468
    :param external_diff_options: If set, use an external GNU diff and pass 
 
469
        these options.
 
470
    :param extra_trees: If set, more Trees to use for looking up file ids
 
471
    :param path_encoding: If set, the path will be encoded as specified, 
 
472
        otherwise is supposed to be utf8
 
473
    :param format_cls: Formatter class (DiffTree subclass)
432
474
    """
 
475
    if format_cls is None:
 
476
        format_cls = DiffTree
433
477
    old_tree.lock_read()
434
478
    try:
435
479
        if extra_trees is not None:
437
481
                tree.lock_read()
438
482
        new_tree.lock_read()
439
483
        try:
440
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
441
 
                                                 path_encoding,
442
 
                                                 external_diff_options,
443
 
                                                 old_label, new_label, using)
 
484
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
485
                                                   path_encoding,
 
486
                                                   external_diff_options,
 
487
                                                   old_label, new_label, using)
444
488
            return differ.show_diff(specific_files, extra_trees)
445
489
        finally:
446
490
            new_tree.unlock()
657
701
        """
658
702
        def _get_text(tree, file_id, path):
659
703
            if file_id is not None:
660
 
                return tree.get_file(file_id, path).readlines()
 
704
                return tree.get_file_lines(file_id, path)
661
705
            else:
662
706
                return []
663
707
        try:
664
708
            from_text = _get_text(self.old_tree, from_file_id, from_path)
665
709
            to_text = _get_text(self.new_tree, to_file_id, to_path)
666
710
            self.text_differ(from_label, from_text, to_label, to_text,
667
 
                             self.to_file)
 
711
                             self.to_file, path_encoding=self.path_encoding)
668
712
        except errors.BinaryFile:
669
713
            self.to_file.write(
670
714
                  ("Binary files %s and %s differ\n" %
671
 
                  (from_label, to_label)).encode(self.path_encoding))
 
715
                  (from_label, to_label)).encode(self.path_encoding,'replace'))
672
716
        return self.CHANGED
673
717
 
674
718
 
690
734
                     path_encoding)
691
735
 
692
736
    @classmethod
693
 
    def make_from_diff_tree(klass, command_string):
 
737
    def make_from_diff_tree(klass, command_string, external_diff_options=None):
694
738
        def from_diff_tree(diff_tree):
695
 
            return klass.from_string(command_string, diff_tree.old_tree,
 
739
            full_command_string = [command_string]
 
740
            if external_diff_options is not None:
 
741
                full_command_string += ' ' + external_diff_options
 
742
            return klass.from_string(full_command_string, diff_tree.old_tree,
696
743
                                     diff_tree.new_tree, diff_tree.to_file)
697
744
        return from_diff_tree
698
745
 
699
746
    def _get_command(self, old_path, new_path):
700
747
        my_map = {'old_path': old_path, 'new_path': new_path}
701
 
        return [AtTemplate(t).substitute(my_map) for t in
702
 
                self.command_template]
 
748
        command = [AtTemplate(t).substitute(my_map) for t in
 
749
                   self.command_template]
 
750
        if sys.platform == 'win32': # Popen doesn't accept unicode on win32
 
751
            command_encoded = []
 
752
            for c in command:
 
753
                if isinstance(c, unicode):
 
754
                    command_encoded.append(c.encode('mbcs'))
 
755
                else:
 
756
                    command_encoded.append(c)
 
757
            return command_encoded
 
758
        else:
 
759
            return command
703
760
 
704
761
    def _execute(self, old_path, new_path):
705
762
        command = self._get_command(old_path, new_path)
725
782
                raise
726
783
        return True
727
784
 
 
785
    @staticmethod
 
786
    def _fenc():
 
787
        """Returns safe encoding for passing file path to diff tool"""
 
788
        if sys.platform == 'win32':
 
789
            return 'mbcs'
 
790
        else:
 
791
            # Don't fallback to 'utf-8' because subprocess may not be able to
 
792
            # handle utf-8 correctly when locale is not utf-8.
 
793
            return sys.getfilesystemencoding() or 'ascii'
 
794
 
 
795
    def _is_safepath(self, path):
 
796
        """Return true if `path` may be able to pass to subprocess."""
 
797
        fenc = self._fenc()
 
798
        try:
 
799
            return path == path.encode(fenc).decode(fenc)
 
800
        except UnicodeError:
 
801
            return False
 
802
 
 
803
    def _safe_filename(self, prefix, relpath):
 
804
        """Replace unsafe character in `relpath` then join `self._root`,
 
805
        `prefix` and `relpath`."""
 
806
        fenc = self._fenc()
 
807
        # encoded_str.replace('?', '_') may break multibyte char.
 
808
        # So we should encode, decode, then replace(u'?', u'_')
 
809
        relpath_tmp = relpath.encode(fenc, 'replace').decode(fenc, 'replace')
 
810
        relpath_tmp = relpath_tmp.replace(u'?', u'_')
 
811
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
 
812
 
728
813
    def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
729
814
                    allow_write=False):
730
815
        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)
 
816
            full_path = tree.abspath(tree.id2path(file_id))
 
817
            if self._is_safepath(full_path):
 
818
                return full_path
 
819
 
 
820
        full_path = self._safe_filename(prefix, relpath)
734
821
        if not force_temp and self._try_symlink_root(tree, prefix):
735
822
            return full_path
736
823
        parent_dir = osutils.dirname(full_path)
748
835
                target.close()
749
836
        finally:
750
837
            source.close()
 
838
        try:
 
839
            mtime = tree.get_file_mtime(file_id)
 
840
        except errors.FileTimestampUnavailable:
 
841
            pass
 
842
        else:
 
843
            os.utime(full_path, (mtime, mtime))
751
844
        if not allow_write:
752
845
            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
846
        return full_path
759
847
 
760
848
    def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
792
880
        """
793
881
        old_path = self.old_tree.id2path(file_id)
794
882
        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))
 
883
        old_abs_path, new_abs_path = self._prepare_files(
 
884
                                            file_id, old_path, new_path,
 
885
                                            allow_write_new=True,
 
886
                                            force_temp=True)
 
887
        command = self._get_command(old_abs_path, new_abs_path)
800
888
        subprocess.call(command, cwd=self._root)
801
 
        new_file = open(new_abs_path, 'r')
 
889
        new_file = open(new_abs_path, 'rb')
802
890
        try:
803
891
            return new_file.read()
804
892
        finally:
854
942
        """Factory for producing a DiffTree.
855
943
 
856
944
        Designed to accept options used by show_diff_trees.
 
945
 
857
946
        :param old_tree: The tree to show as old in the comparison
858
947
        :param new_tree: The tree to show as new in the comparison
859
948
        :param to_file: File to write comparisons to
865
954
        :param using: Commandline to use to invoke an external diff tool
866
955
        """
867
956
        if using is not None:
868
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using)]
 
957
            extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
869
958
        else:
870
959
            extra_factories = []
871
960
        if external_diff_options:
872
961
            opts = external_diff_options.split()
873
 
            def diff_file(olab, olines, nlab, nlines, to_file):
 
962
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None):
 
963
                """:param path_encoding: not used but required
 
964
                        to match the signature of internal_diff.
 
965
                """
874
966
                external_diff(olab, olines, nlab, nlines, to_file, opts)
875
967
        else:
876
968
            diff_file = internal_diff
882
974
    def show_diff(self, specific_files, extra_trees=None):
883
975
        """Write tree diff to self.to_file
884
976
 
885
 
        :param sepecific_files: the specific files to compare (recursive)
 
977
        :param specific_files: the specific files to compare (recursive)
886
978
        :param extra_trees: extra trees to use for mapping paths to file_ids
887
979
        """
888
980
        try:
978
1070
            if error_path is None:
979
1071
                error_path = old_path
980
1072
            raise errors.NoDiffFound(error_path)
 
1073
 
 
1074
 
 
1075
format_registry = Registry()
 
1076
format_registry.register('default', DiffTree)