/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: Breezy landing bot
  • Author(s): Colin Watson
  • Date: 2020-11-16 21:47:08 UTC
  • mfrom: (7521.1.1 remove-lp-workaround)
  • Revision ID: breezy.the.bot@gmail.com-20201116214708-jos209mgxi41oy15
Remove breezy.git workaround for bazaar.launchpad.net.

Merged from https://code.launchpad.net/~cjwatson/brz/remove-lp-workaround/+merge/393710

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