~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Vincent Ladeuil
  • Date: 2010-04-23 08:51:52 UTC
  • mfrom: (5131.2.6 support_OO_flag)
  • mto: This revision was merged to the branch mainline in revision 5179.
  • Revision ID: v.ladeuil+lp@free.fr-20100423085152-uoewc1vnkwqhw0pj
Manually assign docstrings to command objects, so that they work with python -OO

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
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
30
31
from bzrlib import (
31
32
    branch as _mod_branch,
32
33
    bzrdir,
33
 
    commands,
 
34
    cmdline,
 
35
    cleanup,
34
36
    errors,
35
37
    osutils,
36
38
    patiencediff,
38
40
    timestamp,
39
41
    views,
40
42
    )
 
43
 
 
44
from bzrlib.workingtree import WorkingTree
41
45
""")
42
46
 
 
47
from bzrlib.registry import (
 
48
    Registry,
 
49
    )
43
50
from bzrlib.symbol_versioning import (
44
51
    deprecated_function,
 
52
    deprecated_in,
45
53
    )
46
54
from bzrlib.trace import mutter, note, warning
47
55
 
48
56
 
 
57
class AtTemplate(string.Template):
 
58
    """Templating class that uses @ instead of $."""
 
59
 
 
60
    delimiter = '@'
 
61
 
 
62
 
49
63
# TODO: Rather than building a changeset object, we should probably
50
64
# invoke callbacks on an object.  That object can either accumulate a
51
65
# list, write them out directly, etc etc.
171
185
 
172
186
        if not diff_opts:
173
187
            diff_opts = []
 
188
        if sys.platform == 'win32':
 
189
            # Popen doesn't do the proper encoding for external commands
 
190
            # Since we are dealing with an ANSI api, use mbcs encoding
 
191
            old_filename = old_filename.encode('mbcs')
 
192
            new_filename = new_filename.encode('mbcs')
174
193
        diffcmd = ['diff',
175
194
                   '--label', old_filename,
176
195
                   old_abspath,
272
291
                        new_abspath, e)
273
292
 
274
293
 
275
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url,
276
 
    apply_view=True):
277
 
    """Get the trees and specific files to diff given a list of paths.
278
 
 
279
 
    This method works out the trees to be diff'ed and the files of
280
 
    interest within those trees.
281
 
 
282
 
    :param path_list:
283
 
        the list of arguments passed to the diff command
284
 
    :param revision_specs:
285
 
        Zero, one or two RevisionSpecs from the diff command line,
286
 
        saying what revisions to compare.
287
 
    :param old_url:
288
 
        The url of the old branch or tree. If None, the tree to use is
289
 
        taken from the first path, if any, or the current working tree.
290
 
    :param new_url:
291
 
        The url of the new branch or tree. If None, the tree to use is
292
 
        taken from the first path, if any, or the current working tree.
293
 
    :param apply_view:
294
 
        if True and a view is set, apply the view or check that the paths
295
 
        are within it
296
 
    :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.
 
294
@deprecated_function(deprecated_in((2, 2, 0)))
 
295
def get_trees_and_branches_to_diff(path_list, revision_specs, old_url, new_url,
 
296
                                   apply_view=True):
 
297
    """Get the trees and specific files to diff given a list of paths.
 
298
 
 
299
    This method works out the trees to be diff'ed and the files of
 
300
    interest within those trees.
 
301
 
 
302
    :param path_list:
 
303
        the list of arguments passed to the diff command
 
304
    :param revision_specs:
 
305
        Zero, one or two RevisionSpecs from the diff command line,
 
306
        saying what revisions to compare.
 
307
    :param old_url:
 
308
        The url of the old branch or tree. If None, the tree to use is
 
309
        taken from the first path, if any, or the current working tree.
 
310
    :param new_url:
 
311
        The url of the new branch or tree. If None, the tree to use is
 
312
        taken from the first path, if any, or the current working tree.
 
313
    :param apply_view:
 
314
        if True and a view is set, apply the view or check that the paths
 
315
        are within it
 
316
    :returns:
 
317
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
318
        specific_files, extra_trees) where extra_trees is a sequence of
 
319
        additional trees to search in for file-ids.  The trees and branches
 
320
        are not locked.
 
321
    """
 
322
    op = cleanup.OperationWithCleanups(get_trees_and_branches_to_diff_locked)
 
323
    return op.run_simple(path_list, revision_specs, old_url, new_url,
 
324
            op.add_cleanup, apply_view=apply_view)
 
325
    
 
326
 
 
327
def get_trees_and_branches_to_diff_locked(
 
328
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
329
    """Get the trees and specific files to diff given a list of paths.
 
330
 
 
331
    This method works out the trees to be diff'ed and the files of
 
332
    interest within those trees.
 
333
 
 
334
    :param path_list:
 
335
        the list of arguments passed to the diff command
 
336
    :param revision_specs:
 
337
        Zero, one or two RevisionSpecs from the diff command line,
 
338
        saying what revisions to compare.
 
339
    :param old_url:
 
340
        The url of the old 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 new_url:
 
343
        The url of the new branch or tree. If None, the tree to use is
 
344
        taken from the first path, if any, or the current working tree.
 
345
    :param add_cleanup:
 
346
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
347
        will register cleanups that must be run to unlock the trees, etc.
 
348
    :param apply_view:
 
349
        if True and a view is set, apply the view or check that the paths
 
350
        are within it
 
351
    :returns:
 
352
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
353
        specific_files, extra_trees) where extra_trees is a sequence of
 
354
        additional trees to search in for file-ids.  The trees and branches
 
355
        will be read-locked until the cleanups registered via the add_cleanup
 
356
        param are run.
300
357
    """
301
358
    # Get the old and new revision specs
302
359
    old_revision_spec = None
325
382
        default_location = path_list[0]
326
383
        other_paths = path_list[1:]
327
384
 
 
385
    def lock_tree_or_branch(wt, br):
 
386
        if wt is not None:
 
387
            wt.lock_read()
 
388
            add_cleanup(wt.unlock)
 
389
        elif br is not None:
 
390
            br.lock_read()
 
391
            add_cleanup(br.unlock)
 
392
 
328
393
    # Get the old location
329
394
    specific_files = []
330
395
    if old_url is None:
331
396
        old_url = default_location
332
397
    working_tree, branch, relpath = \
333
398
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
 
399
    lock_tree_or_branch(working_tree, branch)
334
400
    if consider_relpath and relpath != '':
335
401
        if working_tree is not None and apply_view:
336
402
            views.check_path_in_view(working_tree, relpath)
337
403
        specific_files.append(relpath)
338
404
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
 
405
    old_branch = branch
339
406
 
340
407
    # Get the new location
341
408
    if new_url is None:
343
410
    if new_url != old_url:
344
411
        working_tree, branch, relpath = \
345
412
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
 
413
        lock_tree_or_branch(working_tree, branch)
346
414
        if consider_relpath and relpath != '':
347
415
            if working_tree is not None and apply_view:
348
416
                views.check_path_in_view(working_tree, relpath)
349
417
            specific_files.append(relpath)
350
418
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
351
419
        basis_is_default=working_tree is None)
 
420
    new_branch = branch
352
421
 
353
422
    # Get the specific files (all files is None, no files is [])
354
423
    if make_paths_wt_relative and working_tree is not None:
367
436
            if view_files:
368
437
                specific_files = view_files
369
438
                view_str = views.view_display_str(view_files)
370
 
                note("*** ignoring files outside view: %s" % view_str)
 
439
                note("*** Ignoring files outside view. View is %s" % view_str)
371
440
 
372
441
    # Get extra trees that ought to be searched for file-ids
373
442
    extra_trees = None
374
443
    if working_tree is not None and working_tree not in (old_tree, new_tree):
375
444
        extra_trees = (working_tree,)
376
 
    return old_tree, new_tree, specific_files, extra_trees
 
445
    return old_tree, new_tree, old_branch, new_branch, specific_files, extra_trees
 
446
 
377
447
 
378
448
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
379
449
    if branch is None and tree is not None:
394
464
                    old_label='a/', new_label='b/',
395
465
                    extra_trees=None,
396
466
                    path_encoding='utf8',
397
 
                    using=None):
 
467
                    using=None,
 
468
                    format_cls=None):
398
469
    """Show in text form the changes from one tree to another.
399
470
 
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
 
471
    :param to_file: The output stream.
 
472
    :param specific_files:Include only changes to these files - None for all
 
473
        changes.
 
474
    :param external_diff_options: If set, use an external GNU diff and pass 
 
475
        these options.
 
476
    :param extra_trees: If set, more Trees to use for looking up file ids
 
477
    :param path_encoding: If set, the path will be encoded as specified, 
 
478
        otherwise is supposed to be utf8
 
479
    :param format_cls: Formatter class (DiffTree subclass)
415
480
    """
 
481
    if format_cls is None:
 
482
        format_cls = DiffTree
416
483
    old_tree.lock_read()
417
484
    try:
418
485
        if extra_trees is not None:
420
487
                tree.lock_read()
421
488
        new_tree.lock_read()
422
489
        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)
 
490
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
491
                                                   path_encoding,
 
492
                                                   external_diff_options,
 
493
                                                   old_label, new_label, using)
427
494
            return differ.show_diff(specific_files, extra_trees)
428
495
        finally:
429
496
            new_tree.unlock()
436
503
 
437
504
def _patch_header_date(tree, file_id, path):
438
505
    """Returns a timestamp suitable for use in a patch header."""
439
 
    mtime = tree.get_file_mtime(file_id, path)
 
506
    try:
 
507
        mtime = tree.get_file_mtime(file_id, path)
 
508
    except errors.FileTimestampUnavailable:
 
509
        mtime = 0
440
510
    return timestamp.format_patch_date(mtime)
441
511
 
442
512
 
620
690
            return self.CANNOT_DIFF
621
691
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
622
692
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
623
 
        return self.diff_text(from_file_id, to_file_id, from_label, to_label)
 
693
        return self.diff_text(from_file_id, to_file_id, from_label, to_label,
 
694
            old_path, new_path)
624
695
 
625
 
    def diff_text(self, from_file_id, to_file_id, from_label, to_label):
 
696
    def diff_text(self, from_file_id, to_file_id, from_label, to_label,
 
697
        from_path=None, to_path=None):
626
698
        """Diff the content of given files in two trees
627
699
 
628
700
        :param from_file_id: The id of the file in the from tree.  If None,
630
702
        :param to_file_id: The id of the file in the to tree.  This may refer
631
703
            to a different file from from_file_id.  If None,
632
704
            the file is not present in the to tree.
 
705
        :param from_path: The path in the from tree or None if unknown.
 
706
        :param to_path: The path in the to tree or None if unknown.
633
707
        """
634
 
        def _get_text(tree, file_id):
 
708
        def _get_text(tree, file_id, path):
635
709
            if file_id is not None:
636
 
                return tree.get_file(file_id).readlines()
 
710
                return tree.get_file(file_id, path).readlines()
637
711
            else:
638
712
                return []
639
713
        try:
640
 
            from_text = _get_text(self.old_tree, from_file_id)
641
 
            to_text = _get_text(self.new_tree, to_file_id)
 
714
            from_text = _get_text(self.old_tree, from_file_id, from_path)
 
715
            to_text = _get_text(self.new_tree, to_file_id, to_path)
642
716
            self.text_differ(from_label, from_text, to_label, to_text,
643
717
                             self.to_file)
644
718
        except errors.BinaryFile:
659
733
    @classmethod
660
734
    def from_string(klass, command_string, old_tree, new_tree, to_file,
661
735
                    path_encoding='utf-8'):
662
 
        command_template = commands.shlex_split_unicode(command_string)
663
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
 
736
        command_template = cmdline.split(command_string)
 
737
        if '@' not in command_string:
 
738
            command_template.extend(['@old_path', '@new_path'])
664
739
        return klass(command_template, old_tree, new_tree, to_file,
665
740
                     path_encoding)
666
741
 
673
748
 
674
749
    def _get_command(self, old_path, new_path):
675
750
        my_map = {'old_path': old_path, 'new_path': new_path}
676
 
        return [t % my_map for t in self.command_template]
 
751
        return [AtTemplate(t).substitute(my_map) for t in
 
752
                self.command_template]
677
753
 
678
754
    def _execute(self, old_path, new_path):
679
755
        command = self._get_command(old_path, new_path)
699
775
                raise
700
776
        return True
701
777
 
702
 
    def _write_file(self, file_id, tree, prefix, relpath):
 
778
    def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
 
779
                    allow_write=False):
 
780
        if not force_temp and isinstance(tree, WorkingTree):
 
781
            return tree.abspath(tree.id2path(file_id))
 
782
        
703
783
        full_path = osutils.pathjoin(self._root, prefix, relpath)
704
 
        if self._try_symlink_root(tree, prefix):
 
784
        if not force_temp and self._try_symlink_root(tree, prefix):
705
785
            return full_path
706
786
        parent_dir = osutils.dirname(full_path)
707
787
        try:
718
798
                target.close()
719
799
        finally:
720
800
            source.close()
721
 
        osutils.make_readonly(full_path)
722
 
        mtime = tree.get_file_mtime(file_id)
723
 
        os.utime(full_path, (mtime, mtime))
 
801
        try:
 
802
            mtime = tree.get_file_mtime(file_id)
 
803
        except errors.FileTimestampUnavailable:
 
804
            pass
 
805
        else:
 
806
            os.utime(full_path, (mtime, mtime))
 
807
        if not allow_write:
 
808
            osutils.make_readonly(full_path)
724
809
        return full_path
725
810
 
726
 
    def _prepare_files(self, file_id, old_path, new_path):
 
811
    def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
 
812
                       allow_write_new=False):
727
813
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
728
 
                                         old_path)
 
814
                                         old_path, force_temp)
729
815
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
730
 
                                         new_path)
 
816
                                         new_path, force_temp,
 
817
                                         allow_write=allow_write_new)
731
818
        return old_disk_path, new_disk_path
732
819
 
733
820
    def finish(self):
734
 
        osutils.rmtree(self._root)
 
821
        try:
 
822
            osutils.rmtree(self._root)
 
823
        except OSError, e:
 
824
            if e.errno != errno.ENOENT:
 
825
                mutter("The temporary directory \"%s\" was not "
 
826
                        "cleanly removed: %s." % (self._root, e))
735
827
 
736
828
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
737
829
        if (old_kind, new_kind) != ('file', 'file'):
738
830
            return DiffPath.CANNOT_DIFF
739
 
        self._prepare_files(file_id, old_path, new_path)
740
 
        self._execute(osutils.pathjoin('old', old_path),
741
 
                      osutils.pathjoin('new', new_path))
 
831
        (old_disk_path, new_disk_path) = self._prepare_files(
 
832
                                                file_id, old_path, new_path)
 
833
        self._execute(old_disk_path, new_disk_path)
 
834
 
 
835
    def edit_file(self, file_id):
 
836
        """Use this tool to edit a file.
 
837
 
 
838
        A temporary copy will be edited, and the new contents will be
 
839
        returned.
 
840
 
 
841
        :param file_id: The id of the file to edit.
 
842
        :return: The new contents of the file.
 
843
        """
 
844
        old_path = self.old_tree.id2path(file_id)
 
845
        new_path = self.new_tree.id2path(file_id)
 
846
        new_abs_path = self._prepare_files(file_id, old_path, new_path,
 
847
                                           allow_write_new=True,
 
848
                                           force_temp=True)[1]
 
849
        command = self._get_command(osutils.pathjoin('old', old_path),
 
850
                                    osutils.pathjoin('new', new_path))
 
851
        subprocess.call(command, cwd=self._root)
 
852
        new_file = open(new_abs_path, 'r')
 
853
        try:
 
854
            return new_file.read()
 
855
        finally:
 
856
            new_file.close()
742
857
 
743
858
 
744
859
class DiffTree(object):
818
933
    def show_diff(self, specific_files, extra_trees=None):
819
934
        """Write tree diff to self.to_file
820
935
 
821
 
        :param sepecific_files: the specific files to compare (recursive)
 
936
        :param specific_files: the specific files to compare (recursive)
822
937
        :param extra_trees: extra trees to use for mapping paths to file_ids
823
938
        """
824
939
        try:
882
997
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
883
998
                                   newpath_encoded, prop_str))
884
999
            if changed_content:
885
 
                self.diff(file_id, oldpath, newpath)
 
1000
                self._diff(file_id, oldpath, newpath, kind[0], kind[1])
886
1001
                has_changes = 1
887
1002
            if renamed:
888
1003
                has_changes = 1
903
1018
            new_kind = self.new_tree.kind(file_id)
904
1019
        except (errors.NoSuchId, errors.NoSuchFile):
905
1020
            new_kind = None
906
 
 
 
1021
        self._diff(file_id, old_path, new_path, old_kind, new_kind)
 
1022
 
 
1023
 
 
1024
    def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
907
1025
        result = DiffPath._diff_many(self.differs, file_id, old_path,
908
1026
                                       new_path, old_kind, new_kind)
909
1027
        if result is DiffPath.CANNOT_DIFF:
911
1029
            if error_path is None:
912
1030
                error_path = old_path
913
1031
            raise errors.NoDiffFound(error_path)
 
1032
 
 
1033
 
 
1034
format_registry = Registry()
 
1035
format_registry.register('default', DiffTree)