~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Aaron Bentley
  • Date: 2007-12-09 23:53:50 UTC
  • mto: This revision was merged to the branch mainline in revision 3133.
  • Revision ID: aaron.bentley@utoronto.ca-20071209235350-qp39yk0xzx7a4f6p
Don't use the base if not cherrypicking

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
import difflib
18
18
import os
19
19
import re
20
 
import shutil
21
20
import sys
22
21
 
23
22
from bzrlib.lazy_import import lazy_import
28
27
import time
29
28
 
30
29
from bzrlib import (
31
 
    branch as _mod_branch,
32
 
    bzrdir,
33
 
    commands,
34
30
    errors,
35
31
    osutils,
36
32
    patiencediff,
41
37
 
42
38
from bzrlib.symbol_versioning import (
43
39
        deprecated_function,
44
 
        one_three
45
40
        )
46
 
from bzrlib.trace import warning
 
41
from bzrlib.trace import mutter, warning
47
42
 
48
43
 
49
44
# TODO: Rather than building a changeset object, we should probably
99
94
        ud[2] = ud[2].replace('-1,0', '-0,0')
100
95
    elif not newlines:
101
96
        ud[2] = ud[2].replace('+1,0', '+0,0')
 
97
    # work around for difflib emitting random spaces after the label
 
98
    ud[0] = ud[0][:-2] + '\n'
 
99
    ud[1] = ud[1][:-2] + '\n'
102
100
 
103
101
    for line in ud:
104
102
        to_file.write(line)
272
270
                        new_abspath, e)
273
271
 
274
272
 
275
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
276
 
    """Get the trees and specific files to diff given a list of paths.
277
 
 
278
 
    This method works out the trees to be diff'ed and the files of
279
 
    interest within those trees.
280
 
 
281
 
    :param path_list:
282
 
        the list of arguments passed to the diff command
283
 
    :param revision_specs:
284
 
        Zero, one or two RevisionSpecs from the diff command line,
285
 
        saying what revisions to compare.
286
 
    :param old_url:
287
 
        The url of the old branch or tree. If None, the tree to use is
288
 
        taken from the first path, if any, or the current working tree.
289
 
    :param new_url:
290
 
        The url of the new branch or tree. If None, the tree to use is
291
 
        taken from the first path, if any, or the current working tree.
292
 
    :returns:
293
 
        a tuple of (old_tree, new_tree, specific_files, extra_trees) where
294
 
        extra_trees is a sequence of additional trees to search in for
295
 
        file-ids.
 
273
def diff_cmd_helper(tree, specific_files, external_diff_options, 
 
274
                    old_revision_spec=None, new_revision_spec=None,
 
275
                    revision_specs=None,
 
276
                    old_label='a/', new_label='b/'):
 
277
    """Helper for cmd_diff.
 
278
 
 
279
    :param tree:
 
280
        A WorkingTree
 
281
 
 
282
    :param specific_files:
 
283
        The specific files to compare, or None
 
284
 
 
285
    :param external_diff_options:
 
286
        If non-None, run an external diff, and pass it these options
 
287
 
 
288
    :param old_revision_spec:
 
289
        If None, use basis tree as old revision, otherwise use the tree for
 
290
        the specified revision. 
 
291
 
 
292
    :param new_revision_spec:
 
293
        If None, use working tree as new revision, otherwise use the tree for
 
294
        the specified revision.
 
295
    
 
296
    :param revision_specs: 
 
297
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
 
298
        to compare.  This can be passed as an alternative to the old_revision_spec 
 
299
        and new_revision_spec parameters.
 
300
 
 
301
    The more general form is show_diff_trees(), where the caller
 
302
    supplies any two trees.
296
303
    """
297
 
    # Get the old and new revision specs
298
 
    old_revision_spec = None
299
 
    new_revision_spec = None
 
304
 
 
305
    # TODO: perhaps remove the old parameters old_revision_spec and
 
306
    # new_revision_spec, since this is only really for use from cmd_diff and
 
307
    # it now always passes through a sequence of revision_specs -- mbp
 
308
    # 20061221
 
309
 
 
310
    def spec_tree(spec):
 
311
        if tree:
 
312
            revision = spec.in_store(tree.branch)
 
313
        else:
 
314
            revision = spec.in_store(None)
 
315
        revision_id = revision.rev_id
 
316
        branch = revision.branch
 
317
        return branch.repository.revision_tree(revision_id)
 
318
 
300
319
    if revision_specs is not None:
 
320
        assert (old_revision_spec is None
 
321
                and new_revision_spec is None)
301
322
        if len(revision_specs) > 0:
302
323
            old_revision_spec = revision_specs[0]
303
 
            if old_url is None:
304
 
                old_url = old_revision_spec.get_branch()
305
324
        if len(revision_specs) > 1:
306
325
            new_revision_spec = revision_specs[1]
307
 
            if new_url is None:
308
 
                new_url = new_revision_spec.get_branch()
309
 
 
310
 
    other_paths = []
311
 
    make_paths_wt_relative = True
312
 
    consider_relpath = True
313
 
    if path_list is None or len(path_list) == 0:
314
 
        # If no path is given, the current working tree is used
315
 
        default_location = u'.'
316
 
        consider_relpath = False
317
 
    elif old_url is not None and new_url is not None:
318
 
        other_paths = path_list
319
 
        make_paths_wt_relative = False
320
 
    else:
321
 
        default_location = path_list[0]
322
 
        other_paths = path_list[1:]
323
 
 
324
 
    # Get the old location
325
 
    specific_files = []
326
 
    if old_url is None:
327
 
        old_url = default_location
328
 
    working_tree, branch, relpath = \
329
 
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
330
 
    if consider_relpath and relpath != '':
331
 
        specific_files.append(relpath)
332
 
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
333
 
 
334
 
    # Get the new location
335
 
    if new_url is None:
336
 
        new_url = default_location
337
 
    if new_url != old_url:
338
 
        working_tree, branch, relpath = \
339
 
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
340
 
        if consider_relpath and relpath != '':
341
 
            specific_files.append(relpath)
342
 
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
343
 
        basis_is_default=working_tree is None)
344
 
 
345
 
    # Get the specific files (all files is None, no files is [])
346
 
    if make_paths_wt_relative and working_tree is not None:
347
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
348
 
    specific_files.extend(other_paths)
349
 
    if len(specific_files) == 0:
350
 
        specific_files = None
351
 
 
352
 
    # Get extra trees that ought to be searched for file-ids
353
 
    extra_trees = None
354
 
    if working_tree is not None and working_tree not in (old_tree, new_tree):
355
 
        extra_trees = (working_tree,)
356
 
    return old_tree, new_tree, specific_files, extra_trees
357
 
 
358
 
 
359
 
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
360
 
    if branch is None and tree is not None:
361
 
        branch = tree.branch
362
 
    if spec is None or spec.spec is None:
363
 
        if basis_is_default:
364
 
            if tree is not None:
365
 
                return tree.basis_tree()
366
 
            else:
367
 
                return branch.basis_tree()
368
 
        else:
369
 
            return tree
370
 
    return spec.as_tree(branch)
371
 
 
372
 
 
373
 
def _relative_paths_in_tree(tree, paths):
374
 
    """Get the relative paths within a working tree.
375
 
 
376
 
    Each path may be either an absolute path or a path relative to the
377
 
    current working directory.
378
 
    """
379
 
    result = []
380
 
    for filename in paths:
381
 
        try:
382
 
            result.append(tree.relpath(osutils.dereference_path(filename)))
383
 
        except errors.PathNotChild:
384
 
            raise errors.BzrCommandError("Files are in different branches")
385
 
    return result
 
326
 
 
327
    if old_revision_spec is None:
 
328
        old_tree = tree.basis_tree()
 
329
    else:
 
330
        old_tree = spec_tree(old_revision_spec)
 
331
 
 
332
    if (new_revision_spec is None
 
333
        or new_revision_spec.spec is None):
 
334
        new_tree = tree
 
335
    else:
 
336
        new_tree = spec_tree(new_revision_spec)
 
337
 
 
338
    if new_tree is not tree:
 
339
        extra_trees = (tree,)
 
340
    else:
 
341
        extra_trees = None
 
342
 
 
343
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
 
344
                           external_diff_options,
 
345
                           old_label=old_label, new_label=new_label,
 
346
                           extra_trees=extra_trees)
386
347
 
387
348
 
388
349
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
389
350
                    external_diff_options=None,
390
351
                    old_label='a/', new_label='b/',
391
352
                    extra_trees=None,
392
 
                    path_encoding='utf8',
393
 
                    using=None):
 
353
                    path_encoding='utf8'):
394
354
    """Show in text form the changes from one tree to another.
395
355
 
396
 
    to_file
397
 
        The output stream.
398
 
 
399
 
    specific_files
400
 
        Include only changes to these files - None for all changes.
 
356
    to_files
 
357
        If set, include only changes to these files.
401
358
 
402
359
    external_diff_options
403
360
        If set, use an external GNU diff and pass these options.
417
374
        new_tree.lock_read()
418
375
        try:
419
376
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
420
 
                                                 path_encoding,
421
 
                                                 external_diff_options,
422
 
                                                 old_label, new_label, using)
 
377
                                                   path_encoding,
 
378
                                                   external_diff_options,
 
379
                                                   old_label, new_label)
423
380
            return differ.show_diff(specific_files, extra_trees)
424
381
        finally:
425
382
            new_tree.unlock()
433
390
def _patch_header_date(tree, file_id, path):
434
391
    """Returns a timestamp suitable for use in a patch header."""
435
392
    mtime = tree.get_file_mtime(file_id, path)
 
393
    assert mtime is not None, \
 
394
        "got an mtime of None for file-id %s, path %s in tree %s" % (
 
395
                file_id, path, tree)
436
396
    return timestamp.format_patch_date(mtime)
437
397
 
438
398
 
439
 
@deprecated_function(one_three)
 
399
def _raise_if_nonexistent(paths, old_tree, new_tree):
 
400
    """Complain if paths are not in either inventory or tree.
 
401
 
 
402
    It's OK with the files exist in either tree's inventory, or 
 
403
    if they exist in the tree but are not versioned.
 
404
    
 
405
    This can be used by operations such as bzr status that can accept
 
406
    unknown or ignored files.
 
407
    """
 
408
    mutter("check paths: %r", paths)
 
409
    if not paths:
 
410
        return
 
411
    s = old_tree.filter_unversioned_files(paths)
 
412
    s = new_tree.filter_unversioned_files(s)
 
413
    s = [path for path in s if not new_tree.has_filename(path)]
 
414
    if s:
 
415
        raise errors.PathsDoNotExist(sorted(s))
 
416
 
 
417
 
440
418
def get_prop_change(meta_modified):
441
419
    if meta_modified:
442
420
        return " (properties changed)"
443
421
    else:
444
422
        return  ""
445
423
 
446
 
def get_executable_change(old_is_x, new_is_x):
447
 
    descr = { True:"+x", False:"-x", None:"??" }
448
 
    if old_is_x != new_is_x:
449
 
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
450
 
    else:
451
 
        return []
452
 
 
453
424
 
454
425
class DiffPath(object):
455
426
    """Base type for command object that compare files"""
474
445
        self.to_file = to_file
475
446
        self.path_encoding = path_encoding
476
447
 
477
 
    def finish(self):
478
 
        pass
479
 
 
480
448
    @classmethod
481
449
    def from_diff_tree(klass, diff_tree):
482
450
        return klass(diff_tree.old_tree, diff_tree.new_tree,
502
470
    def __init__(self, differs):
503
471
        self.differs = differs
504
472
 
505
 
    def finish(self):
506
 
        pass
507
 
 
508
473
    @classmethod
509
474
    def from_diff_tree(klass, diff_tree):
510
475
        return klass(diff_tree.differs)
651
616
        return self.CHANGED
652
617
 
653
618
 
654
 
class DiffFromTool(DiffPath):
655
 
 
656
 
    def __init__(self, command_template, old_tree, new_tree, to_file,
657
 
                 path_encoding='utf-8'):
658
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
659
 
        self.command_template = command_template
660
 
        self._root = osutils.mkdtemp(prefix='bzr-diff-')
661
 
 
662
 
    @classmethod
663
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
664
 
                    path_encoding='utf-8'):
665
 
        command_template = commands.shlex_split_unicode(command_string)
666
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
667
 
        return klass(command_template, old_tree, new_tree, to_file,
668
 
                     path_encoding)
669
 
 
670
 
    @classmethod
671
 
    def make_from_diff_tree(klass, command_string):
672
 
        def from_diff_tree(diff_tree):
673
 
            return klass.from_string(command_string, diff_tree.old_tree,
674
 
                                     diff_tree.new_tree, diff_tree.to_file)
675
 
        return from_diff_tree
676
 
 
677
 
    def _get_command(self, old_path, new_path):
678
 
        my_map = {'old_path': old_path, 'new_path': new_path}
679
 
        return [t % my_map for t in self.command_template]
680
 
 
681
 
    def _execute(self, old_path, new_path):
682
 
        command = self._get_command(old_path, new_path)
683
 
        try:
684
 
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
685
 
                                    cwd=self._root)
686
 
        except OSError, e:
687
 
            if e.errno == errno.ENOENT:
688
 
                raise errors.ExecutableMissing(command[0])
689
 
            else:
690
 
                raise
691
 
        self.to_file.write(proc.stdout.read())
692
 
        return proc.wait()
693
 
 
694
 
    def _try_symlink_root(self, tree, prefix):
695
 
        if (getattr(tree, 'abspath', None) is None
696
 
            or not osutils.host_os_dereferences_symlinks()):
697
 
            return False
698
 
        try:
699
 
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
700
 
        except OSError, e:
701
 
            if e.errno != errno.EEXIST:
702
 
                raise
703
 
        return True
704
 
 
705
 
    def _write_file(self, file_id, tree, prefix, relpath):
706
 
        full_path = osutils.pathjoin(self._root, prefix, relpath)
707
 
        if self._try_symlink_root(tree, prefix):
708
 
            return full_path
709
 
        parent_dir = osutils.dirname(full_path)
710
 
        try:
711
 
            os.makedirs(parent_dir)
712
 
        except OSError, e:
713
 
            if e.errno != errno.EEXIST:
714
 
                raise
715
 
        source = tree.get_file(file_id, relpath)
716
 
        try:
717
 
            target = open(full_path, 'wb')
718
 
            try:
719
 
                osutils.pumpfile(source, target)
720
 
            finally:
721
 
                target.close()
722
 
        finally:
723
 
            source.close()
724
 
        osutils.make_readonly(full_path)
725
 
        mtime = tree.get_file_mtime(file_id)
726
 
        os.utime(full_path, (mtime, mtime))
727
 
        return full_path
728
 
 
729
 
    def _prepare_files(self, file_id, old_path, new_path):
730
 
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
731
 
                                         old_path)
732
 
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
733
 
                                         new_path)
734
 
        return old_disk_path, new_disk_path
735
 
 
736
 
    def finish(self):
737
 
        osutils.rmtree(self._root)
738
 
 
739
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
740
 
        if (old_kind, new_kind) != ('file', 'file'):
741
 
            return DiffPath.CANNOT_DIFF
742
 
        self._prepare_files(file_id, old_path, new_path)
743
 
        self._execute(osutils.pathjoin('old', old_path),
744
 
                      osutils.pathjoin('new', new_path))
745
 
 
746
 
 
747
619
class DiffTree(object):
748
620
    """Provides textual representations of the difference between two trees.
749
621
 
789
661
    @classmethod
790
662
    def from_trees_options(klass, old_tree, new_tree, to_file,
791
663
                           path_encoding, external_diff_options, old_label,
792
 
                           new_label, using):
 
664
                           new_label):
793
665
        """Factory for producing a DiffTree.
794
666
 
795
667
        Designed to accept options used by show_diff_trees.
801
673
            binary to perform file comparison, using supplied options.
802
674
        :param old_label: Prefix to use for old file labels
803
675
        :param new_label: Prefix to use for new file labels
804
 
        :param using: Commandline to use to invoke an external diff tool
805
676
        """
806
 
        if using is not None:
807
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using)]
808
 
        else:
809
 
            extra_factories = []
810
677
        if external_diff_options:
 
678
            assert isinstance(external_diff_options, basestring)
811
679
            opts = external_diff_options.split()
812
680
            def diff_file(olab, olines, nlab, nlines, to_file):
813
681
                external_diff(olab, olines, nlab, nlines, to_file, opts)
815
683
            diff_file = internal_diff
816
684
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
817
685
                             old_label, new_label, diff_file)
818
 
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
819
 
                     extra_factories)
 
686
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text)
820
687
 
821
688
    def show_diff(self, specific_files, extra_trees=None):
822
689
        """Write tree diff to self.to_file
824
691
        :param sepecific_files: the specific files to compare (recursive)
825
692
        :param extra_trees: extra trees to use for mapping paths to file_ids
826
693
        """
827
 
        try:
828
 
            return self._show_diff(specific_files, extra_trees)
829
 
        finally:
830
 
            for differ in self.differs:
831
 
                differ.finish()
832
 
 
833
 
    def _show_diff(self, specific_files, extra_trees):
834
694
        # TODO: Generation of pseudo-diffs for added/deleted files could
835
695
        # be usefully made into a much faster special case.
836
 
        iterator = self.new_tree.iter_changes(self.old_tree,
837
 
                                               specific_files=specific_files,
838
 
                                               extra_trees=extra_trees,
839
 
                                               require_versioned=True)
 
696
 
 
697
        delta = self.new_tree.changes_from(self.old_tree,
 
698
            specific_files=specific_files,
 
699
            extra_trees=extra_trees, require_versioned=True)
 
700
 
840
701
        has_changes = 0
841
 
        def changes_key(change):
842
 
            old_path, new_path = change[1]
843
 
            path = new_path
844
 
            if path is None:
845
 
                path = old_path
846
 
            return path
847
 
        def get_encoded_path(path):
848
 
            if path is not None:
849
 
                return path.encode(self.path_encoding, "replace")
850
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
851
 
             executable) in sorted(iterator, key=changes_key):
852
 
            # The root does not get diffed, and items with no known kind (that
853
 
            # is, missing) in both trees are skipped as well.
854
 
            if parent == (None, None) or kind == (None, None):
855
 
                continue
856
 
            oldpath, newpath = paths
857
 
            oldpath_encoded = get_encoded_path(paths[0])
858
 
            newpath_encoded = get_encoded_path(paths[1])
859
 
            old_present = (kind[0] is not None and versioned[0])
860
 
            new_present = (kind[1] is not None and versioned[1])
861
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
862
 
 
863
 
            properties_changed = []
864
 
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
865
 
 
866
 
            if properties_changed:
867
 
                prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
868
 
            else:
869
 
                prop_str = ""
870
 
 
871
 
            if (old_present, new_present) == (True, False):
872
 
                self.to_file.write("=== removed %s '%s'\n" %
873
 
                                   (kind[0], oldpath_encoded))
874
 
                newpath = oldpath
875
 
            elif (old_present, new_present) == (False, True):
876
 
                self.to_file.write("=== added %s '%s'\n" %
877
 
                                   (kind[1], newpath_encoded))
878
 
                oldpath = newpath
879
 
            elif renamed:
880
 
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
881
 
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
882
 
            else:
883
 
                # if it was produced by iter_changes, it must be
884
 
                # modified *somehow*, either content or execute bit.
885
 
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
886
 
                                   newpath_encoded, prop_str))
887
 
            if changed_content:
888
 
                self.diff(file_id, oldpath, newpath)
889
 
                has_changes = 1
890
 
            if renamed:
891
 
                has_changes = 1
 
702
        for path, file_id, kind in delta.removed:
 
703
            has_changes = 1
 
704
            path_encoded = path.encode(self.path_encoding, "replace")
 
705
            self.to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
 
706
            self.diff(file_id, path, path)
 
707
 
 
708
        for path, file_id, kind in delta.added:
 
709
            has_changes = 1
 
710
            path_encoded = path.encode(self.path_encoding, "replace")
 
711
            self.to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
 
712
            self.diff(file_id, path, path)
 
713
        for (old_path, new_path, file_id, kind,
 
714
             text_modified, meta_modified) in delta.renamed:
 
715
            has_changes = 1
 
716
            prop_str = get_prop_change(meta_modified)
 
717
            oldpath_encoded = old_path.encode(self.path_encoding, "replace")
 
718
            newpath_encoded = new_path.encode(self.path_encoding, "replace")
 
719
            self.to_file.write("=== renamed %s '%s' => '%s'%s\n" % (kind,
 
720
                                oldpath_encoded, newpath_encoded, prop_str))
 
721
            if text_modified:
 
722
                self.diff(file_id, old_path, new_path)
 
723
        for path, file_id, kind, text_modified, meta_modified in\
 
724
            delta.modified:
 
725
            has_changes = 1
 
726
            prop_str = get_prop_change(meta_modified)
 
727
            path_encoded = path.encode(self.path_encoding, "replace")
 
728
            self.to_file.write("=== modified %s '%s'%s\n" % (kind,
 
729
                                path_encoded, prop_str))
 
730
            # The file may be in a different location in the old tree (because
 
731
            # the containing dir was renamed, but the file itself was not)
 
732
            if text_modified:
 
733
                old_path = self.old_tree.id2path(file_id)
 
734
                self.diff(file_id, old_path, path)
892
735
        return has_changes
893
736
 
894
737
    def diff(self, file_id, old_path, new_path):