/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-04-05 19:11:34 UTC
  • mto: (7490.7.16 work)
  • mto: This revision was merged to the branch mainline in revision 7501.
  • Revision ID: jelmer@jelmer.uk-20200405191134-0aebh8ikiwygxma5
Populate the .gitignore file.

Show diffs side-by-side

added added

removed removed

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