/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-06-15 13:39:46 UTC
  • mto: This revision was merged to the branch mainline in revision 7342.
  • Revision ID: jelmer@jelmer.uk-20190615133946-uywh9ix0lfpqw0hy
Install quilt.

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