/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 bzrlib/diff.py

  • Committer: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

Show diffs side-by-side

added added

removed removed

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