/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: 2019-07-07 17:22:07 UTC
  • mfrom: (7363 work)
  • mto: This revision was merged to the branch mainline in revision 7378.
  • Revision ID: jelmer@jelmer.uk-20190707172207-nnugeuwvxsxo62wa
merge trunk.

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