/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: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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