/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Martin von Gagern
  • Date: 2010-04-20 08:47:38 UTC
  • mfrom: (5167 +trunk)
  • mto: This revision was merged to the branch mainline in revision 5195.
  • Revision ID: martin.vgagern@gmx.net-20100420084738-ygymnqmdllzrhpfn
merge 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
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,
37
39
    textfile,
38
40
    timestamp,
 
41
    views,
39
42
    )
 
43
 
 
44
from bzrlib.workingtree import WorkingTree
40
45
""")
41
46
 
 
47
from bzrlib.registry import (
 
48
    Registry,
 
49
    )
42
50
from bzrlib.symbol_versioning import (
43
 
        deprecated_function,
44
 
        one_three
45
 
        )
46
 
from bzrlib.trace import mutter, warning
 
51
    deprecated_function,
 
52
    deprecated_in,
 
53
    )
 
54
from bzrlib.trace import mutter, note, warning
 
55
 
 
56
 
 
57
class AtTemplate(string.Template):
 
58
    """Templating class that uses @ instead of $."""
 
59
 
 
60
    delimiter = '@'
47
61
 
48
62
 
49
63
# TODO: Rather than building a changeset object, we should probably
78
92
    # both sequences are empty.
79
93
    if not oldlines and not newlines:
80
94
        return
81
 
    
 
95
 
82
96
    if allow_binary is False:
83
97
        textfile.check_text_lines(oldlines)
84
98
        textfile.check_text_lines(newlines)
99
113
        ud[2] = ud[2].replace('-1,0', '-0,0')
100
114
    elif not newlines:
101
115
        ud[2] = ud[2].replace('+1,0', '+0,0')
102
 
    # work around for difflib emitting random spaces after the label
103
 
    ud[0] = ud[0][:-2] + '\n'
104
 
    ud[1] = ud[1][:-2] + '\n'
105
116
 
106
117
    for line in ud:
107
118
        to_file.write(line)
174
185
 
175
186
        if not diff_opts:
176
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')
177
193
        diffcmd = ['diff',
178
194
                   '--label', old_filename,
179
195
                   old_abspath,
202
218
            break
203
219
        else:
204
220
            diffcmd.append('-u')
205
 
                  
 
221
 
206
222
        if diff_opts:
207
223
            diffcmd.extend(diff_opts)
208
224
 
209
225
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
210
226
        out,err = pipe.communicate()
211
227
        rc = pipe.returncode
212
 
        
 
228
 
213
229
        # internal_diff() adds a trailing newline, add one here for consistency
214
230
        out += '\n'
215
231
        if rc == 2:
250
266
                msg = 'signal %d' % (-rc)
251
267
            else:
252
268
                msg = 'exit code %d' % rc
253
 
                
254
 
            raise errors.BzrError('external diff failed with %s; command: %r' 
 
269
 
 
270
            raise errors.BzrError('external diff failed with %s; command: %r'
255
271
                                  % (rc, diffcmd))
256
272
 
257
273
 
275
291
                        new_abspath, e)
276
292
 
277
293
 
278
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
279
 
    """Get the trees and specific files to diff given a list of paths.
280
 
 
281
 
    This method works out the trees to be diff'ed and the files of
282
 
    interest within those trees.
283
 
 
284
 
    :param path_list:
285
 
        the list of arguments passed to the diff command
286
 
    :param revision_specs:
287
 
        Zero, one or two RevisionSpecs from the diff command line,
288
 
        saying what revisions to compare.
289
 
    :param old_url:
290
 
        The url of the old branch or tree. If None, the tree to use is
291
 
        taken from the first path, if any, or the current working tree.
292
 
    :param new_url:
293
 
        The url of the new branch or tree. If None, the tree to use is
294
 
        taken from the first path, if any, or the current working tree.
295
 
    :returns:
296
 
        a tuple of (old_tree, new_tree, specific_files, extra_trees) where
297
 
        extra_trees is a sequence of additional trees to search in for
298
 
        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.
299
357
    """
300
358
    # Get the old and new revision specs
301
359
    old_revision_spec = None
324
382
        default_location = path_list[0]
325
383
        other_paths = path_list[1:]
326
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
 
327
393
    # Get the old location
328
394
    specific_files = []
329
395
    if old_url is None:
330
396
        old_url = default_location
331
397
    working_tree, branch, relpath = \
332
398
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
 
399
    lock_tree_or_branch(working_tree, branch)
333
400
    if consider_relpath and relpath != '':
 
401
        if working_tree is not None and apply_view:
 
402
            views.check_path_in_view(working_tree, relpath)
334
403
        specific_files.append(relpath)
335
404
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
 
405
    old_branch = branch
336
406
 
337
407
    # Get the new location
338
408
    if new_url is None:
340
410
    if new_url != old_url:
341
411
        working_tree, branch, relpath = \
342
412
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
 
413
        lock_tree_or_branch(working_tree, branch)
343
414
        if consider_relpath and relpath != '':
 
415
            if working_tree is not None and apply_view:
 
416
                views.check_path_in_view(working_tree, relpath)
344
417
            specific_files.append(relpath)
345
418
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
346
419
        basis_is_default=working_tree is None)
 
420
    new_branch = branch
347
421
 
348
422
    # Get the specific files (all files is None, no files is [])
349
423
    if make_paths_wt_relative and working_tree is not None:
350
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
 
424
        try:
 
425
            from bzrlib.builtins import safe_relpath_files
 
426
            other_paths = safe_relpath_files(working_tree, other_paths,
 
427
            apply_view=apply_view)
 
428
        except errors.FileInWrongBranch:
 
429
            raise errors.BzrCommandError("Files are in different branches")
351
430
    specific_files.extend(other_paths)
352
431
    if len(specific_files) == 0:
353
432
        specific_files = None
 
433
        if (working_tree is not None and working_tree.supports_views()
 
434
            and apply_view):
 
435
            view_files = working_tree.views.lookup_view()
 
436
            if view_files:
 
437
                specific_files = view_files
 
438
                view_str = views.view_display_str(view_files)
 
439
                note("*** Ignoring files outside view. View is %s" % view_str)
354
440
 
355
441
    # Get extra trees that ought to be searched for file-ids
356
442
    extra_trees = None
357
443
    if working_tree is not None and working_tree not in (old_tree, new_tree):
358
444
        extra_trees = (working_tree,)
359
 
    return old_tree, new_tree, specific_files, extra_trees
 
445
    return old_tree, new_tree, old_branch, new_branch, specific_files, extra_trees
360
446
 
361
447
 
362
448
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
373
459
    return spec.as_tree(branch)
374
460
 
375
461
 
376
 
def _relative_paths_in_tree(tree, paths):
377
 
    """Get the relative paths within a working tree.
378
 
 
379
 
    Each path may be either an absolute path or a path relative to the
380
 
    current working directory.
381
 
    """
382
 
    result = []
383
 
    for filename in paths:
384
 
        try:
385
 
            result.append(tree.relpath(osutils.dereference_path(filename)))
386
 
        except errors.PathNotChild:
387
 
            raise errors.BzrCommandError("Files are in different branches")
388
 
    return result
389
 
 
390
 
 
391
462
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
392
463
                    external_diff_options=None,
393
464
                    old_label='a/', new_label='b/',
394
465
                    extra_trees=None,
395
466
                    path_encoding='utf8',
396
 
                    using=None):
 
467
                    using=None,
 
468
                    format_cls=None):
397
469
    """Show in text form the changes from one tree to another.
398
470
 
399
 
    to_file
400
 
        The output stream.
401
 
 
402
 
    specific_files
403
 
        Include only changes to these files - None for all changes.
404
 
 
405
 
    external_diff_options
406
 
        If set, use an external GNU diff and pass these options.
407
 
 
408
 
    extra_trees
409
 
        If set, more Trees to use for looking up file ids
410
 
 
411
 
    path_encoding
412
 
        If set, the path will be encoded as specified, otherwise is supposed
413
 
        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)
414
480
    """
 
481
    if format_cls is None:
 
482
        format_cls = DiffTree
415
483
    old_tree.lock_read()
416
484
    try:
417
485
        if extra_trees is not None:
419
487
                tree.lock_read()
420
488
        new_tree.lock_read()
421
489
        try:
422
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
423
 
                                                 path_encoding,
424
 
                                                 external_diff_options,
425
 
                                                 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)
426
494
            return differ.show_diff(specific_files, extra_trees)
427
495
        finally:
428
496
            new_tree.unlock()
435
503
 
436
504
def _patch_header_date(tree, file_id, path):
437
505
    """Returns a timestamp suitable for use in a patch header."""
438
 
    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
439
510
    return timestamp.format_patch_date(mtime)
440
511
 
441
512
 
442
 
def _raise_if_nonexistent(paths, old_tree, new_tree):
443
 
    """Complain if paths are not in either inventory or tree.
444
 
 
445
 
    It's OK with the files exist in either tree's inventory, or 
446
 
    if they exist in the tree but are not versioned.
447
 
    
448
 
    This can be used by operations such as bzr status that can accept
449
 
    unknown or ignored files.
450
 
    """
451
 
    mutter("check paths: %r", paths)
452
 
    if not paths:
453
 
        return
454
 
    s = old_tree.filter_unversioned_files(paths)
455
 
    s = new_tree.filter_unversioned_files(s)
456
 
    s = [path for path in s if not new_tree.has_filename(path)]
457
 
    if s:
458
 
        raise errors.PathsDoNotExist(sorted(s))
459
 
 
460
 
 
461
 
@deprecated_function(one_three)
462
 
def get_prop_change(meta_modified):
463
 
    if meta_modified:
464
 
        return " (properties changed)"
465
 
    else:
466
 
        return  ""
467
 
 
468
513
def get_executable_change(old_is_x, new_is_x):
469
514
    descr = { True:"+x", False:"-x", None:"??" }
470
515
    if old_is_x != new_is_x:
645
690
            return self.CANNOT_DIFF
646
691
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
647
692
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
648
 
        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)
649
695
 
650
 
    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):
651
698
        """Diff the content of given files in two trees
652
699
 
653
700
        :param from_file_id: The id of the file in the from tree.  If None,
655
702
        :param to_file_id: The id of the file in the to tree.  This may refer
656
703
            to a different file from from_file_id.  If None,
657
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.
658
707
        """
659
 
        def _get_text(tree, file_id):
 
708
        def _get_text(tree, file_id, path):
660
709
            if file_id is not None:
661
 
                return tree.get_file(file_id).readlines()
 
710
                return tree.get_file(file_id, path).readlines()
662
711
            else:
663
712
                return []
664
713
        try:
665
 
            from_text = _get_text(self.old_tree, from_file_id)
666
 
            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)
667
716
            self.text_differ(from_label, from_text, to_label, to_text,
668
717
                             self.to_file)
669
718
        except errors.BinaryFile:
684
733
    @classmethod
685
734
    def from_string(klass, command_string, old_tree, new_tree, to_file,
686
735
                    path_encoding='utf-8'):
687
 
        command_template = commands.shlex_split_unicode(command_string)
688
 
        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'])
689
739
        return klass(command_template, old_tree, new_tree, to_file,
690
740
                     path_encoding)
691
741
 
698
748
 
699
749
    def _get_command(self, old_path, new_path):
700
750
        my_map = {'old_path': old_path, 'new_path': new_path}
701
 
        return [t % my_map for t in self.command_template]
 
751
        return [AtTemplate(t).substitute(my_map) for t in
 
752
                self.command_template]
702
753
 
703
754
    def _execute(self, old_path, new_path):
704
755
        command = self._get_command(old_path, new_path)
724
775
                raise
725
776
        return True
726
777
 
727
 
    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
        
728
783
        full_path = osutils.pathjoin(self._root, prefix, relpath)
729
 
        if self._try_symlink_root(tree, prefix):
 
784
        if not force_temp and self._try_symlink_root(tree, prefix):
730
785
            return full_path
731
786
        parent_dir = osutils.dirname(full_path)
732
787
        try:
743
798
                target.close()
744
799
        finally:
745
800
            source.close()
746
 
        osutils.make_readonly(full_path)
747
 
        mtime = tree.get_file_mtime(file_id)
748
 
        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)
749
809
        return full_path
750
810
 
751
 
    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):
752
813
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
753
 
                                         old_path)
 
814
                                         old_path, force_temp)
754
815
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
755
 
                                         new_path)
 
816
                                         new_path, force_temp,
 
817
                                         allow_write=allow_write_new)
756
818
        return old_disk_path, new_disk_path
757
819
 
758
820
    def finish(self):
759
 
        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))
760
827
 
761
828
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
762
829
        if (old_kind, new_kind) != ('file', 'file'):
763
830
            return DiffPath.CANNOT_DIFF
764
 
        self._prepare_files(file_id, old_path, new_path)
765
 
        self._execute(osutils.pathjoin('old', old_path),
766
 
                      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()
767
857
 
768
858
 
769
859
class DiffTree(object):
843
933
    def show_diff(self, specific_files, extra_trees=None):
844
934
        """Write tree diff to self.to_file
845
935
 
846
 
        :param sepecific_files: the specific files to compare (recursive)
 
936
        :param specific_files: the specific files to compare (recursive)
847
937
        :param extra_trees: extra trees to use for mapping paths to file_ids
848
938
        """
849
939
        try:
907
997
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
908
998
                                   newpath_encoded, prop_str))
909
999
            if changed_content:
910
 
                self.diff(file_id, oldpath, newpath)
 
1000
                self._diff(file_id, oldpath, newpath, kind[0], kind[1])
911
1001
                has_changes = 1
912
1002
            if renamed:
913
1003
                has_changes = 1
928
1018
            new_kind = self.new_tree.kind(file_id)
929
1019
        except (errors.NoSuchId, errors.NoSuchFile):
930
1020
            new_kind = None
931
 
 
 
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):
932
1025
        result = DiffPath._diff_many(self.differs, file_id, old_path,
933
1026
                                       new_path, old_kind, new_kind)
934
1027
        if result is DiffPath.CANNOT_DIFF:
936
1029
            if error_path is None:
937
1030
                error_path = old_path
938
1031
            raise errors.NoDiffFound(error_path)
 
1032
 
 
1033
 
 
1034
format_registry = Registry()
 
1035
format_registry.register('default', DiffTree)