/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 breezy/diff.py

  • Committer: Jelmer Vernooij
  • Date: 2020-05-06 02:13:25 UTC
  • mfrom: (7490.7.21 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200506021325-awbmmqu1zyorz7sj
Merge 3.1 branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
import contextlib
19
18
import difflib
20
19
import os
21
20
import re
22
 
import string
23
21
import sys
24
22
 
25
23
from .lazy_import import lazy_import
26
24
lazy_import(globals(), """
27
25
import errno
 
26
import patiencediff
28
27
import subprocess
29
28
import tempfile
30
29
 
31
30
from breezy import (
32
 
    cleanup,
33
 
    cmdline,
34
31
    controldir,
35
 
    errors,
36
32
    osutils,
37
 
    patiencediff,
38
33
    textfile,
39
34
    timestamp,
40
35
    views,
44
39
from breezy.i18n import gettext
45
40
""")
46
41
 
 
42
from . import (
 
43
    errors,
 
44
    )
47
45
from .registry import (
48
46
    Registry,
49
47
    )
53
51
 
54
52
DEFAULT_CONTEXT_AMOUNT = 3
55
53
 
56
 
class AtTemplate(string.Template):
57
 
    """Templating class that uses @ instead of $."""
58
 
 
59
 
    delimiter = '@'
60
 
 
61
54
 
62
55
# TODO: Rather than building a changeset object, we should probably
63
56
# invoke callbacks on an object.  That object can either accumulate a
73
66
        self.opcodes = None
74
67
 
75
68
 
76
 
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
69
def internal_diff(old_label, oldlines, new_label, newlines, to_file,
77
70
                  allow_binary=False, sequence_matcher=None,
78
71
                  path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
79
72
    # FIXME: difflib is wrong if there is no trailing newline.
86
79
    # In the meantime we at least make sure the patch isn't
87
80
    # mangled.
88
81
 
89
 
 
90
 
    # Special workaround for Python2.3, where difflib fails if
91
 
    # both sequences are empty.
92
 
    if not oldlines and not newlines:
93
 
        return
94
 
 
95
82
    if allow_binary is False:
96
83
        textfile.check_text_lines(oldlines)
97
84
        textfile.check_text_lines(newlines)
98
85
 
99
86
    if sequence_matcher is None:
100
87
        sequence_matcher = patiencediff.PatienceSequenceMatcher
101
 
    ud = patiencediff.unified_diff(oldlines, newlines,
102
 
                      fromfile=old_filename.encode(path_encoding, 'replace'),
103
 
                      tofile=new_filename.encode(path_encoding, 'replace'),
104
 
                      n=context_lines, sequencematcher=sequence_matcher)
 
88
    ud = unified_diff_bytes(
 
89
        oldlines, newlines,
 
90
        fromfile=old_label.encode(path_encoding, 'replace'),
 
91
        tofile=new_label.encode(path_encoding, 'replace'),
 
92
        n=context_lines, sequencematcher=sequence_matcher)
105
93
 
106
94
    ud = list(ud)
107
 
    if len(ud) == 0: # Identical contents, nothing to do
 
95
    if len(ud) == 0:  # Identical contents, nothing to do
108
96
        return
109
97
    # work-around for difflib being too smart for its own good
110
98
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
111
99
    if not oldlines:
112
 
        ud[2] = ud[2].replace('-1,0', '-0,0')
 
100
        ud[2] = ud[2].replace(b'-1,0', b'-0,0')
113
101
    elif not newlines:
114
 
        ud[2] = ud[2].replace('+1,0', '+0,0')
 
102
        ud[2] = ud[2].replace(b'+1,0', b'+0,0')
115
103
 
116
104
    for line in ud:
117
105
        to_file.write(line)
118
 
        if not line.endswith('\n'):
119
 
            to_file.write("\n\\ No newline at end of file\n")
120
 
    to_file.write('\n')
 
106
        if not line.endswith(b'\n'):
 
107
            to_file.write(b"\n\\ No newline at end of file\n")
 
108
    to_file.write(b'\n')
 
109
 
 
110
 
 
111
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
 
112
                       tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
 
113
    r"""
 
114
    Compare two sequences of lines; generate the delta as a unified diff.
 
115
 
 
116
    Unified diffs are a compact way of showing line changes and a few
 
117
    lines of context.  The number of context lines is set by 'n' which
 
118
    defaults to three.
 
119
 
 
120
    By default, the diff control lines (those with ---, +++, or @@) are
 
121
    created with a trailing newline.  This is helpful so that inputs
 
122
    created from file.readlines() result in diffs that are suitable for
 
123
    file.writelines() since both the inputs and outputs have trailing
 
124
    newlines.
 
125
 
 
126
    For inputs that do not have trailing newlines, set the lineterm
 
127
    argument to "" so that the output will be uniformly newline free.
 
128
 
 
129
    The unidiff format normally has a header for filenames and modification
 
130
    times.  Any or all of these may be specified using strings for
 
131
    'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.  The modification
 
132
    times are normally expressed in the format returned by time.ctime().
 
133
 
 
134
    Example:
 
135
 
 
136
    >>> for line in bytes_unified_diff(b'one two three four'.split(),
 
137
    ...             b'zero one tree four'.split(), b'Original', b'Current',
 
138
    ...             b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
 
139
    ...             lineterm=b''):
 
140
    ...     print line
 
141
    --- Original Sat Jan 26 23:30:50 1991
 
142
    +++ Current Fri Jun 06 10:20:52 2003
 
143
    @@ -1,4 +1,4 @@
 
144
    +zero
 
145
     one
 
146
    -two
 
147
    -three
 
148
    +tree
 
149
     four
 
150
    """
 
151
    if sequencematcher is None:
 
152
        sequencematcher = difflib.SequenceMatcher
 
153
 
 
154
    if fromfiledate:
 
155
        fromfiledate = b'\t' + bytes(fromfiledate)
 
156
    if tofiledate:
 
157
        tofiledate = b'\t' + bytes(tofiledate)
 
158
 
 
159
    started = False
 
160
    for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
 
161
        if not started:
 
162
            yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
 
163
            yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
 
164
            started = True
 
165
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
 
166
        yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
 
167
        for tag, i1, i2, j1, j2 in group:
 
168
            if tag == 'equal':
 
169
                for line in a[i1:i2]:
 
170
                    yield b' ' + line
 
171
                continue
 
172
            if tag == 'replace' or tag == 'delete':
 
173
                for line in a[i1:i2]:
 
174
                    yield b'-' + line
 
175
            if tag == 'replace' or tag == 'insert':
 
176
                for line in b[j1:j2]:
 
177
                    yield b'+' + line
121
178
 
122
179
 
123
180
def _spawn_external_diff(diffcmd, capture_errors=True):
156
213
 
157
214
    return pipe
158
215
 
 
216
 
159
217
# diff style options as of GNU diff v3.2
160
218
style_option_list = ['-c', '-C', '--context',
161
219
                     '-e', '--ed',
167
225
                     '-y', '--side-by-side',
168
226
                     '-D', '--ifdef']
169
227
 
 
228
 
170
229
def default_style_unified(diff_opts):
171
230
    """Default to unified diff style if alternative not specified in diff_opts.
172
231
 
191
250
    return diff_opts
192
251
 
193
252
 
194
 
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
253
def external_diff(old_label, oldlines, new_label, newlines, to_file,
195
254
                  diff_opts):
196
255
    """Display a diff by calling out to the external diff program."""
197
256
    # make sure our own output is properly ordered before the diff
221
280
        if sys.platform == 'win32':
222
281
            # Popen doesn't do the proper encoding for external commands
223
282
            # Since we are dealing with an ANSI api, use mbcs encoding
224
 
            old_filename = old_filename.encode('mbcs')
225
 
            new_filename = new_filename.encode('mbcs')
 
283
            old_label = old_label.encode('mbcs')
 
284
            new_label = new_label.encode('mbcs')
226
285
        diffcmd = ['diff',
227
 
                   '--label', old_filename,
 
286
                   '--label', old_label,
228
287
                   old_abspath,
229
 
                   '--label', new_filename,
 
288
                   '--label', new_label,
230
289
                   new_abspath,
231
290
                   '--binary',
232
 
                  ]
 
291
                   ]
233
292
 
234
293
        diff_opts = default_style_unified(diff_opts)
235
294
 
241
300
        rc = pipe.returncode
242
301
 
243
302
        # internal_diff() adds a trailing newline, add one here for consistency
244
 
        out += '\n'
 
303
        out += b'\n'
245
304
        if rc == 2:
246
305
            # 'diff' gives retcode == 2 for all sorts of errors
247
306
            # one of those is 'Binary files differ'.
254
313
            out, err = pipe.communicate()
255
314
 
256
315
            # Write out the new i18n diff response
257
 
            to_file.write(out+'\n')
 
316
            to_file.write(out + b'\n')
258
317
            if pipe.returncode != 2:
259
318
                raise errors.BzrError(
260
 
                               'external diff failed with exit code 2'
261
 
                               ' when run with LANG=C and LC_ALL=C,'
262
 
                               ' but not when run natively: %r' % (diffcmd,))
 
319
                    'external diff failed with exit code 2'
 
320
                    ' when run with LANG=C and LC_ALL=C,'
 
321
                    ' but not when run natively: %r' % (diffcmd,))
263
322
 
264
 
            first_line = lang_c_out.split('\n', 1)[0]
 
323
            first_line = lang_c_out.split(b'\n', 1)[0]
265
324
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
266
 
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
 
325
            m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
267
326
            if m is None:
268
327
                raise errors.BzrError('external diff failed with exit code 2;'
269
328
                                      ' command: %r' % (diffcmd,))
284
343
            raise errors.BzrError('external diff failed with %s; command: %r'
285
344
                                  % (msg, diffcmd))
286
345
 
287
 
 
288
346
    finally:
289
347
        oldtmpf.close()                 # and delete
290
348
        newtmpf.close()
304
362
 
305
363
 
306
364
def get_trees_and_branches_to_diff_locked(
307
 
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
365
        path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
308
366
    """Get the trees and specific files to diff given a list of paths.
309
367
 
310
368
    This method works out the trees to be diff'ed and the files of
321
379
    :param new_url:
322
380
        The url of the new branch or tree. If None, the tree to use is
323
381
        taken from the first path, if any, or the current working tree.
324
 
    :param add_cleanup:
325
 
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
382
    :param exit_stack:
 
383
        an ExitStack object. get_trees_and_branches_to_diff
326
384
        will register cleanups that must be run to unlock the trees, etc.
327
385
    :param apply_view:
328
386
        if True and a view is set, apply the view or check that the paths
331
389
        a tuple of (old_tree, new_tree, old_branch, new_branch,
332
390
        specific_files, extra_trees) where extra_trees is a sequence of
333
391
        additional trees to search in for file-ids.  The trees and branches
334
 
        will be read-locked until the cleanups registered via the add_cleanup
 
392
        will be read-locked until the cleanups registered via the exit_stack
335
393
        param are run.
336
394
    """
337
395
    # Get the old and new revision specs
363
421
 
364
422
    def lock_tree_or_branch(wt, br):
365
423
        if wt is not None:
366
 
            wt.lock_read()
367
 
            add_cleanup(wt.unlock)
 
424
            exit_stack.enter_context(wt.lock_read())
368
425
        elif br is not None:
369
 
            br.lock_read()
370
 
            add_cleanup(br.unlock)
 
426
            exit_stack.enter_context(br.lock_read())
371
427
 
372
428
    # Get the old location
373
429
    specific_files = []
395
451
                views.check_path_in_view(working_tree, relpath)
396
452
            specific_files.append(relpath)
397
453
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
398
 
        basis_is_default=working_tree is None)
 
454
                                 basis_is_default=working_tree is None)
399
455
    new_branch = branch
400
456
 
401
457
    # Get the specific files (all files is None, no files is [])
406
462
    specific_files.extend(other_paths)
407
463
    if len(specific_files) == 0:
408
464
        specific_files = None
409
 
        if (working_tree is not None and working_tree.supports_views()
410
 
            and apply_view):
 
465
        if (working_tree is not None and working_tree.supports_views() and
 
466
                apply_view):
411
467
            view_files = working_tree.views.lookup_view()
412
468
            if view_files:
413
469
                specific_files = view_files
449
505
    :param to_file: The output stream.
450
506
    :param specific_files: Include only changes to these files - None for all
451
507
        changes.
452
 
    :param external_diff_options: If set, use an external GNU diff and pass 
 
508
    :param external_diff_options: If set, use an external GNU diff and pass
453
509
        these options.
454
510
    :param extra_trees: If set, more Trees to use for looking up file ids
455
 
    :param path_encoding: If set, the path will be encoded as specified, 
 
511
    :param path_encoding: If set, the path will be encoded as specified,
456
512
        otherwise is supposed to be utf8
457
513
    :param format_cls: Formatter class (DiffTree subclass)
458
514
    """
460
516
        context = DEFAULT_CONTEXT_AMOUNT
461
517
    if format_cls is None:
462
518
        format_cls = DiffTree
463
 
    with old_tree.lock_read():
 
519
    with contextlib.ExitStack() as exit_stack:
 
520
        exit_stack.enter_context(old_tree.lock_read())
464
521
        if extra_trees is not None:
465
522
            for tree in extra_trees:
466
 
                tree.lock_read()
467
 
        new_tree.lock_read()
468
 
        try:
469
 
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
470
 
                                                   path_encoding,
471
 
                                                   external_diff_options,
472
 
                                                   old_label, new_label, using,
473
 
                                                   context_lines=context)
474
 
            return differ.show_diff(specific_files, extra_trees)
475
 
        finally:
476
 
            new_tree.unlock()
477
 
            if extra_trees is not None:
478
 
                for tree in extra_trees:
479
 
                    tree.unlock()
480
 
 
481
 
 
482
 
def _patch_header_date(tree, file_id, path):
 
523
                exit_stack.enter_context(tree.lock_read())
 
524
        exit_stack.enter_context(new_tree.lock_read())
 
525
        differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
526
                                               path_encoding,
 
527
                                               external_diff_options,
 
528
                                               old_label, new_label, using,
 
529
                                               context_lines=context)
 
530
        return differ.show_diff(specific_files, extra_trees)
 
531
 
 
532
 
 
533
def _patch_header_date(tree, path):
483
534
    """Returns a timestamp suitable for use in a patch header."""
484
535
    try:
485
 
        mtime = tree.get_file_mtime(path, file_id)
 
536
        mtime = tree.get_file_mtime(path)
486
537
    except FileTimestampUnavailable:
487
538
        mtime = 0
488
539
    return timestamp.format_patch_date(mtime)
489
540
 
490
541
 
491
542
def get_executable_change(old_is_x, new_is_x):
492
 
    descr = { True:"+x", False:"-x", None:"??" }
 
543
    descr = {True: b"+x", False: b"-x", None: b"??"}
493
544
    if old_is_x != new_is_x:
494
 
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
 
545
        return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
495
546
    else:
496
547
        return []
497
548
 
528
579
                     diff_tree.to_file, diff_tree.path_encoding)
529
580
 
530
581
    @staticmethod
531
 
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
 
582
    def _diff_many(differs, old_path, new_path, old_kind, new_kind):
532
583
        for file_differ in differs:
533
 
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
534
 
                                      new_kind)
 
584
            result = file_differ.diff(old_path, new_path, old_kind, new_kind)
535
585
            if result is not DiffPath.CANNOT_DIFF:
536
586
                return result
537
587
        else:
544
594
    Represents kind change as deletion + creation.  Uses the other differs
545
595
    to do this.
546
596
    """
 
597
 
547
598
    def __init__(self, differs):
548
599
        self.differs = differs
549
600
 
554
605
    def from_diff_tree(klass, diff_tree):
555
606
        return klass(diff_tree.differs)
556
607
 
557
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
608
    def diff(self, old_path, new_path, old_kind, new_kind):
558
609
        """Perform comparison
559
610
 
560
 
        :param file_id: The file_id of the file to compare
561
611
        :param old_path: Path of the file in the old tree
562
612
        :param new_path: Path of the file in the new tree
563
613
        :param old_kind: Old file-kind of the file
565
615
        """
566
616
        if None in (old_kind, new_kind):
567
617
            return DiffPath.CANNOT_DIFF
568
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
569
 
                                       new_path, old_kind, None)
 
618
        result = DiffPath._diff_many(
 
619
            self.differs, old_path, new_path, old_kind, None)
570
620
        if result is DiffPath.CANNOT_DIFF:
571
621
            return result
572
 
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
573
 
                                     None, new_kind)
 
622
        return DiffPath._diff_many(
 
623
            self.differs, old_path, new_path, None, new_kind)
 
624
 
 
625
 
 
626
class DiffTreeReference(DiffPath):
 
627
 
 
628
    def diff(self, old_path, new_path, old_kind, new_kind):
 
629
        """Perform comparison between two tree references.  (dummy)
 
630
 
 
631
        """
 
632
        if 'tree-reference' not in (old_kind, new_kind):
 
633
            return self.CANNOT_DIFF
 
634
        if old_kind not in ('tree-reference', None):
 
635
            return self.CANNOT_DIFF
 
636
        if new_kind not in ('tree-reference', None):
 
637
            return self.CANNOT_DIFF
 
638
        return self.CHANGED
574
639
 
575
640
 
576
641
class DiffDirectory(DiffPath):
577
642
 
578
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
643
    def diff(self, old_path, new_path, old_kind, new_kind):
579
644
        """Perform comparison between two directories.  (dummy)
580
645
 
581
646
        """
590
655
 
591
656
class DiffSymlink(DiffPath):
592
657
 
593
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
658
    def diff(self, old_path, new_path, old_kind, new_kind):
594
659
        """Perform comparison between two symlinks
595
660
 
596
 
        :param file_id: The file_id of the file to compare
597
661
        :param old_path: Path of the file in the old tree
598
662
        :param new_path: Path of the file in the new tree
599
663
        :param old_kind: Old file-kind of the file
602
666
        if 'symlink' not in (old_kind, new_kind):
603
667
            return self.CANNOT_DIFF
604
668
        if old_kind == 'symlink':
605
 
            old_target = self.old_tree.get_symlink_target(old_path, file_id)
 
669
            old_target = self.old_tree.get_symlink_target(old_path)
606
670
        elif old_kind is None:
607
671
            old_target = None
608
672
        else:
609
673
            return self.CANNOT_DIFF
610
674
        if new_kind == 'symlink':
611
 
            new_target = self.new_tree.get_symlink_target(new_path, file_id)
 
675
            new_target = self.new_tree.get_symlink_target(new_path)
612
676
        elif new_kind is None:
613
677
            new_target = None
614
678
        else:
617
681
 
618
682
    def diff_symlink(self, old_target, new_target):
619
683
        if old_target is None:
620
 
            self.to_file.write('=== target is %r\n' % new_target)
 
684
            self.to_file.write(b'=== target is \'%s\'\n' %
 
685
                               new_target.encode(self.path_encoding, 'replace'))
621
686
        elif new_target is None:
622
 
            self.to_file.write('=== target was %r\n' % old_target)
 
687
            self.to_file.write(b'=== target was \'%s\'\n' %
 
688
                               old_target.encode(self.path_encoding, 'replace'))
623
689
        else:
624
 
            self.to_file.write('=== target changed %r => %r\n' %
625
 
                              (old_target, new_target))
 
690
            self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
 
691
                               (old_target.encode(self.path_encoding, 'replace'),
 
692
                                new_target.encode(self.path_encoding, 'replace')))
626
693
        return self.CHANGED
627
694
 
628
695
 
632
699
    # or removed in a diff.
633
700
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
634
701
 
635
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8', 
636
 
                 old_label='', new_label='', text_differ=internal_diff, 
 
702
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
703
                 old_label='', new_label='', text_differ=internal_diff,
637
704
                 context_lines=DEFAULT_CONTEXT_AMOUNT):
638
705
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
639
706
        self.text_differ = text_differ
642
709
        self.path_encoding = path_encoding
643
710
        self.context_lines = context_lines
644
711
 
645
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
712
    def diff(self, old_path, new_path, old_kind, new_kind):
646
713
        """Compare two files in unified diff format
647
714
 
648
 
        :param file_id: The file_id of the file to compare
649
715
        :param old_path: Path of the file in the old tree
650
716
        :param new_path: Path of the file in the new tree
651
717
        :param old_kind: Old file-kind of the file
653
719
        """
654
720
        if 'file' not in (old_kind, new_kind):
655
721
            return self.CANNOT_DIFF
656
 
        from_file_id = to_file_id = file_id
657
722
        if old_kind == 'file':
658
 
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
 
723
            old_date = _patch_header_date(self.old_tree, old_path)
659
724
        elif old_kind is None:
660
725
            old_date = self.EPOCH_DATE
661
 
            from_file_id = None
662
726
        else:
663
727
            return self.CANNOT_DIFF
664
728
        if new_kind == 'file':
665
 
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
 
729
            new_date = _patch_header_date(self.new_tree, new_path)
666
730
        elif new_kind is None:
667
731
            new_date = self.EPOCH_DATE
668
 
            to_file_id = None
669
732
        else:
670
733
            return self.CANNOT_DIFF
671
 
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
672
 
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
673
 
        return self.diff_text(old_path, new_path, from_label, to_label,
674
 
            from_file_id, to_file_id)
 
734
        from_label = '%s%s\t%s' % (self.old_label, old_path,
 
735
                                   old_date)
 
736
        to_label = '%s%s\t%s' % (self.new_label, new_path,
 
737
                                 new_date)
 
738
        return self.diff_text(old_path, new_path, from_label, to_label)
675
739
 
676
 
    def diff_text(self, from_path, to_path, from_label, to_label,
677
 
        from_file_id=None, to_file_id=None):
 
740
    def diff_text(self, from_path, to_path, from_label, to_label):
678
741
        """Diff the content of given files in two trees
679
742
 
680
743
        :param from_path: The path in the from tree. If None,
682
745
        :param to_path: The path in the to tree. This may refer
683
746
            to a different file from from_path.  If None,
684
747
            the file is not present in the to tree.
685
 
        :param from_file_id: The id of the file in the from tree or None if
686
 
            unknown.
687
 
        :param to_file_id: The id of the file in the to tree or None if
688
 
            unknown.
689
748
        """
690
 
        def _get_text(tree, file_id, path):
691
 
            if file_id is None:
692
 
                return []
693
 
            return tree.get_file_lines(path, file_id)
 
749
        def _get_text(tree, path):
 
750
            if path is None:
 
751
                return []
 
752
            try:
 
753
                return tree.get_file_lines(path)
 
754
            except errors.NoSuchFile:
 
755
                return []
694
756
        try:
695
 
            from_text = _get_text(self.old_tree, from_file_id, from_path)
696
 
            to_text = _get_text(self.new_tree, to_file_id, to_path)
 
757
            from_text = _get_text(self.old_tree, from_path)
 
758
            to_text = _get_text(self.new_tree, to_path)
697
759
            self.text_differ(from_label, from_text, to_label, to_text,
698
760
                             self.to_file, path_encoding=self.path_encoding,
699
761
                             context_lines=self.context_lines)
700
762
        except errors.BinaryFile:
701
763
            self.to_file.write(
702
 
                  ("Binary files %s and %s differ\n" %
703
 
                  (from_label, to_label)).encode(self.path_encoding, 'replace'))
 
764
                ("Binary files %s%s and %s%s differ\n" %
 
765
                 (self.old_label, from_path, self.new_label, to_path)).encode(self.path_encoding, 'replace'))
704
766
        return self.CHANGED
705
767
 
706
768
 
713
775
        self._root = osutils.mkdtemp(prefix='brz-diff-')
714
776
 
715
777
    @classmethod
716
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
 
778
    def from_string(klass, command_template, old_tree, new_tree, to_file,
717
779
                    path_encoding='utf-8'):
718
 
        command_template = cmdline.split(command_string)
719
 
        if '@' not in command_string:
720
 
            command_template.extend(['@old_path', '@new_path'])
721
780
        return klass(command_template, old_tree, new_tree, to_file,
722
781
                     path_encoding)
723
782
 
733
792
 
734
793
    def _get_command(self, old_path, new_path):
735
794
        my_map = {'old_path': old_path, 'new_path': new_path}
736
 
        command = [AtTemplate(t).substitute(my_map) for t in
 
795
        command = [t.format(**my_map) for t in
737
796
                   self.command_template]
738
 
        if sys.platform == 'win32': # Popen doesn't accept unicode on win32
 
797
        if command == self.command_template:
 
798
            command += [old_path, new_path]
 
799
        if sys.platform == 'win32':  # Popen doesn't accept unicode on win32
739
800
            command_encoded = []
740
801
            for c in command:
741
 
                if isinstance(c, unicode):
 
802
                if isinstance(c, str):
742
803
                    command_encoded.append(c.encode('mbcs'))
743
804
                else:
744
805
                    command_encoded.append(c)
757
818
            else:
758
819
                raise
759
820
        self.to_file.write(proc.stdout.read())
 
821
        proc.stdout.close()
760
822
        return proc.wait()
761
823
 
762
824
    def _try_symlink_root(self, tree, prefix):
763
 
        if (getattr(tree, 'abspath', None) is None
764
 
            or not osutils.host_os_dereferences_symlinks()):
 
825
        if (getattr(tree, 'abspath', None) is None or
 
826
                not osutils.host_os_dereferences_symlinks()):
765
827
            return False
766
828
        try:
767
829
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
799
861
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
800
862
 
801
863
    def _write_file(self, relpath, tree, prefix, force_temp=False,
802
 
                    allow_write=False, file_id=None):
 
864
                    allow_write=False):
803
865
        if not force_temp and isinstance(tree, WorkingTree):
804
866
            full_path = tree.abspath(relpath)
805
867
            if self._is_safepath(full_path):
814
876
        except OSError as e:
815
877
            if e.errno != errno.EEXIST:
816
878
                raise
817
 
        source = tree.get_file(relpath, file_id)
818
 
        try:
819
 
            target = open(full_path, 'wb')
820
 
            try:
821
 
                osutils.pumpfile(source, target)
822
 
            finally:
823
 
                target.close()
824
 
        finally:
825
 
            source.close()
826
 
        try:
827
 
            mtime = tree.get_file_mtime(relpath, file_id)
 
879
        with tree.get_file(relpath) as source, \
 
880
                open(full_path, 'wb') as target:
 
881
            osutils.pumpfile(source, target)
 
882
        try:
 
883
            mtime = tree.get_file_mtime(relpath)
828
884
        except FileTimestampUnavailable:
829
885
            pass
830
886
        else:
834
890
        return full_path
835
891
 
836
892
    def _prepare_files(self, old_path, new_path, force_temp=False,
837
 
                       allow_write_new=False, file_id=None):
838
 
        old_disk_path = self._write_file(old_path, self.old_tree, 'old',
839
 
                                         force_temp, file_id=file_id)
840
 
        new_disk_path = self._write_file(new_path, self.new_tree, 'new',
841
 
                                         force_temp, file_id=file_id,
842
 
                                         allow_write=allow_write_new)
 
893
                       allow_write_new=False):
 
894
        old_disk_path = self._write_file(
 
895
            old_path, self.old_tree, 'old', force_temp)
 
896
        new_disk_path = self._write_file(
 
897
            new_path, self.new_tree, 'new', force_temp,
 
898
            allow_write=allow_write_new)
843
899
        return old_disk_path, new_disk_path
844
900
 
845
901
    def finish(self):
848
904
        except OSError as e:
849
905
            if e.errno != errno.ENOENT:
850
906
                mutter("The temporary directory \"%s\" was not "
851
 
                        "cleanly removed: %s." % (self._root, e))
 
907
                       "cleanly removed: %s." % (self._root, e))
852
908
 
853
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
909
    def diff(self, old_path, new_path, old_kind, new_kind):
854
910
        if (old_kind, new_kind) != ('file', 'file'):
855
911
            return DiffPath.CANNOT_DIFF
856
912
        (old_disk_path, new_disk_path) = self._prepare_files(
857
 
                old_path, new_path, file_id=file_id)
 
913
            old_path, new_path)
858
914
        self._execute(old_disk_path, new_disk_path)
859
915
 
860
 
    def edit_file(self, old_path, new_path, file_id=None):
 
916
    def edit_file(self, old_path, new_path):
861
917
        """Use this tool to edit a file.
862
918
 
863
919
        A temporary copy will be edited, and the new contents will be
864
920
        returned.
865
921
 
866
 
        :param file_id: The id of the file to edit.
867
922
        :return: The new contents of the file.
868
923
        """
869
924
        old_abs_path, new_abs_path = self._prepare_files(
870
 
                old_path, new_path, allow_write_new=True, force_temp=True,
871
 
                file_id=file_id)
 
925
            old_path, new_path, allow_write_new=True, force_temp=True)
872
926
        command = self._get_command(old_abs_path, new_abs_path)
873
927
        subprocess.call(command, cwd=self._root)
874
 
        new_file = open(new_abs_path, 'rb')
875
 
        try:
 
928
        with open(new_abs_path, 'rb') as new_file:
876
929
            return new_file.read()
877
 
        finally:
878
 
            new_file.close()
879
930
 
880
931
 
881
932
class DiffTree(object):
893
944
    # list of factories that can provide instances of DiffPath objects
894
945
    # may be extended by plugins.
895
946
    diff_factories = [DiffSymlink.from_diff_tree,
896
 
                      DiffDirectory.from_diff_tree]
 
947
                      DiffDirectory.from_diff_tree,
 
948
                      DiffTreeReference.from_diff_tree]
897
949
 
898
950
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
899
951
                 diff_text=None, extra_factories=None):
909
961
            DiffPaths"""
910
962
        if diff_text is None:
911
963
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
912
 
                                 '', '',  internal_diff)
 
964
                                 '', '', internal_diff)
913
965
        self.old_tree = old_tree
914
966
        self.new_tree = new_tree
915
967
        self.to_file = to_file
939
991
        :param using: Commandline to use to invoke an external diff tool
940
992
        """
941
993
        if using is not None:
942
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
 
994
            extra_factories = [DiffFromTool.make_from_diff_tree(
 
995
                using, external_diff_options)]
943
996
        else:
944
997
            extra_factories = []
945
998
        if external_diff_options:
946
999
            opts = external_diff_options.split()
 
1000
 
947
1001
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
948
1002
                """:param path_encoding: not used but required
949
1003
                        to match the signature of internal_diff.
972
1026
        # TODO: Generation of pseudo-diffs for added/deleted files could
973
1027
        # be usefully made into a much faster special case.
974
1028
        iterator = self.new_tree.iter_changes(self.old_tree,
975
 
                                               specific_files=specific_files,
976
 
                                               extra_trees=extra_trees,
977
 
                                               require_versioned=True)
 
1029
                                              specific_files=specific_files,
 
1030
                                              extra_trees=extra_trees,
 
1031
                                              require_versioned=True)
978
1032
        has_changes = 0
 
1033
 
979
1034
        def changes_key(change):
980
 
            old_path, new_path = change[1]
 
1035
            old_path, new_path = change.path
981
1036
            path = new_path
982
1037
            if path is None:
983
1038
                path = old_path
984
1039
            return path
 
1040
 
985
1041
        def get_encoded_path(path):
986
1042
            if path is not None:
987
1043
                return path.encode(self.path_encoding, "replace")
988
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
989
 
             executable) in sorted(iterator, key=changes_key):
 
1044
        for change in sorted(iterator, key=changes_key):
990
1045
            # The root does not get diffed, and items with no known kind (that
991
1046
            # is, missing) in both trees are skipped as well.
992
 
            if parent == (None, None) or kind == (None, None):
993
 
                continue
994
 
            oldpath, newpath = paths
995
 
            oldpath_encoded = get_encoded_path(paths[0])
996
 
            newpath_encoded = get_encoded_path(paths[1])
997
 
            old_present = (kind[0] is not None and versioned[0])
998
 
            new_present = (kind[1] is not None and versioned[1])
999
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
 
1047
            if change.parent_id == (None, None) or change.kind == (None, None):
 
1048
                continue
 
1049
            if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
 
1050
                warning(
 
1051
                    'Ignoring "%s" as symlinks are not '
 
1052
                    'supported on this filesystem.' % (change.path[0],))
 
1053
                continue
 
1054
            oldpath, newpath = change.path
 
1055
            oldpath_encoded = get_encoded_path(change.path[0])
 
1056
            newpath_encoded = get_encoded_path(change.path[1])
 
1057
            old_present = (change.kind[0] is not None and change.versioned[0])
 
1058
            new_present = (change.kind[1] is not None and change.versioned[1])
 
1059
            executable = change.executable
 
1060
            kind = change.kind
 
1061
            renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
1000
1062
 
1001
1063
            properties_changed = []
1002
 
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
 
1064
            properties_changed.extend(
 
1065
                get_executable_change(executable[0], executable[1]))
1003
1066
 
1004
1067
            if properties_changed:
1005
 
                prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
 
1068
                prop_str = b" (properties changed: %s)" % (
 
1069
                    b", ".join(properties_changed),)
1006
1070
            else:
1007
 
                prop_str = ""
 
1071
                prop_str = b""
1008
1072
 
1009
1073
            if (old_present, new_present) == (True, False):
1010
 
                self.to_file.write("=== removed %s '%s'\n" %
1011
 
                                   (kind[0], oldpath_encoded))
 
1074
                self.to_file.write(b"=== removed %s '%s'\n" %
 
1075
                                   (kind[0].encode('ascii'), oldpath_encoded))
1012
1076
                newpath = oldpath
1013
1077
            elif (old_present, new_present) == (False, True):
1014
 
                self.to_file.write("=== added %s '%s'\n" %
1015
 
                                   (kind[1], newpath_encoded))
 
1078
                self.to_file.write(b"=== added %s '%s'\n" %
 
1079
                                   (kind[1].encode('ascii'), newpath_encoded))
1016
1080
                oldpath = newpath
1017
1081
            elif renamed:
1018
 
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
1019
 
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
 
1082
                self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
 
1083
                                   (kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1020
1084
            else:
1021
1085
                # if it was produced by iter_changes, it must be
1022
1086
                # modified *somehow*, either content or execute bit.
1023
 
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
1024
 
                                   newpath_encoded, prop_str))
1025
 
            if changed_content:
1026
 
                self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
 
1087
                self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
 
1088
                                                                  newpath_encoded, prop_str))
 
1089
            if change.changed_content:
 
1090
                self._diff(oldpath, newpath, kind[0], kind[1])
1027
1091
                has_changes = 1
1028
1092
            if renamed:
1029
1093
                has_changes = 1
1030
1094
        return has_changes
1031
1095
 
1032
 
    def diff(self, file_id, old_path, new_path):
 
1096
    def diff(self, old_path, new_path):
1033
1097
        """Perform a diff of a single file
1034
1098
 
1035
 
        :param file_id: file-id of the file
1036
1099
        :param old_path: The path of the file in the old tree
1037
1100
        :param new_path: The path of the file in the new tree
1038
1101
        """
1039
1102
        if old_path is None:
1040
1103
            old_kind = None
1041
1104
        else:
1042
 
            old_kind = self.old_tree.kind(old_path, file_id)
 
1105
            old_kind = self.old_tree.kind(old_path)
1043
1106
        if new_path is None:
1044
1107
            new_kind = None
1045
1108
        else:
1046
 
            new_kind = self.new_tree.kind(new_path, file_id)
1047
 
        self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
 
1109
            new_kind = self.new_tree.kind(new_path)
 
1110
        self._diff(old_path, new_path, old_kind, new_kind)
1048
1111
 
1049
 
    def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
1050
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
1051
 
                                       new_path, old_kind, new_kind)
 
1112
    def _diff(self, old_path, new_path, old_kind, new_kind):
 
1113
        result = DiffPath._diff_many(
 
1114
            self.differs, old_path, new_path, old_kind, new_kind)
1052
1115
        if result is DiffPath.CANNOT_DIFF:
1053
1116
            error_path = new_path
1054
1117
            if error_path is None: