/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: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2019-07-07 19:22:03 UTC
  • mfrom: (7358.8.5 diff-binary-weird)
  • Revision ID: breezy.the.bot@gmail.com-20190707192203-n32v0wih963qdi6i
Don't include datestamps in filenames when reporting on binary files.

Merged from https://code.launchpad.net/~jelmer/brz/diff-binary-weird/+merge/369474

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
2
 
 
 
1
# Copyright (C) 2005-2014 Canonical Ltd.
 
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
12
 
 
 
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
from bzrlib.delta import compare_trees
18
 
from bzrlib.errors import BzrError
19
 
import bzrlib.errors as errors
20
 
from bzrlib.symbol_versioning import *
21
 
from bzrlib.trace import mutter
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
from __future__ import absolute_import
 
18
 
 
19
import difflib
 
20
import os
 
21
import re
 
22
import sys
 
23
 
 
24
from .lazy_import import lazy_import
 
25
lazy_import(globals(), """
 
26
import errno
 
27
import patiencediff
 
28
import subprocess
 
29
import tempfile
 
30
 
 
31
from breezy import (
 
32
    controldir,
 
33
    errors,
 
34
    osutils,
 
35
    textfile,
 
36
    timestamp,
 
37
    views,
 
38
    )
 
39
 
 
40
from breezy.workingtree import WorkingTree
 
41
from breezy.i18n import gettext
 
42
""")
 
43
 
 
44
from .registry import (
 
45
    Registry,
 
46
    )
 
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
 
53
 
22
54
 
23
55
# TODO: Rather than building a changeset object, we should probably
24
56
# invoke callbacks on an object.  That object can either accumulate a
25
57
# list, write them out directly, etc etc.
26
58
 
27
 
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file):
28
 
    import difflib
29
 
    
 
59
 
 
60
class _PrematchedMatcher(difflib.SequenceMatcher):
 
61
    """Allow SequenceMatcher operations to use predetermined blocks"""
 
62
 
 
63
    def __init__(self, matching_blocks):
 
64
        difflib.SequenceMatcher(self, None, None)
 
65
        self.matching_blocks = matching_blocks
 
66
        self.opcodes = None
 
67
 
 
68
 
 
69
def internal_diff(old_label, oldlines, new_label, newlines, to_file,
 
70
                  allow_binary=False, sequence_matcher=None,
 
71
                  path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
30
72
    # FIXME: difflib is wrong if there is no trailing newline.
31
73
    # The syntax used by patch seems to be "\ No newline at
32
74
    # end of file" following the last diff line from that
37
79
    # In the meantime we at least make sure the patch isn't
38
80
    # mangled.
39
81
 
40
 
 
41
 
    # Special workaround for Python2.3, where difflib fails if
42
 
    # both sequences are empty.
43
 
    if not oldlines and not newlines:
44
 
        return
45
 
 
46
 
    ud = difflib.unified_diff(oldlines, newlines,
47
 
                              fromfile=old_filename+'\t', 
48
 
                              tofile=new_filename+'\t')
 
82
    if allow_binary is False:
 
83
        textfile.check_text_lines(oldlines)
 
84
        textfile.check_text_lines(newlines)
 
85
 
 
86
    if sequence_matcher is None:
 
87
        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)
49
93
 
50
94
    ud = list(ud)
 
95
    if len(ud) == 0:  # Identical contents, nothing to do
 
96
        return
51
97
    # work-around for difflib being too smart for its own good
52
98
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
53
99
    if not oldlines:
54
 
        ud[2] = ud[2].replace('-1,0', '-0,0')
 
100
        ud[2] = ud[2].replace(b'-1,0', b'-0,0')
55
101
    elif not newlines:
56
 
        ud[2] = ud[2].replace('+1,0', '+0,0')
57
 
    # work around for difflib emitting random spaces after the label
58
 
    ud[0] = ud[0][:-2] + '\n'
59
 
    ud[1] = ud[1][:-2] + '\n'
 
102
        ud[2] = ud[2].replace(b'+1,0', b'+0,0')
60
103
 
61
104
    for line in ud:
62
105
        to_file.write(line)
63
 
        if not line.endswith('\n'):
64
 
            to_file.write("\n\\ No newline at end of file\n")
65
 
    print >>to_file
66
 
 
67
 
 
68
 
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
 
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
 
178
 
 
179
 
 
180
def _spawn_external_diff(diffcmd, capture_errors=True):
 
181
    """Spawn the external diff process, and return the child handle.
 
182
 
 
183
    :param diffcmd: The command list to spawn
 
184
    :param capture_errors: Capture stderr as well as setting LANG=C
 
185
        and LC_ALL=C. This lets us read and understand the output of diff,
 
186
        and respond to any errors.
 
187
    :return: A Popen object.
 
188
    """
 
189
    if capture_errors:
 
190
        # construct minimal environment
 
191
        env = {}
 
192
        path = os.environ.get('PATH')
 
193
        if path is not None:
 
194
            env['PATH'] = path
 
195
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
 
196
        env['LANG'] = 'C'
 
197
        env['LC_ALL'] = 'C'
 
198
        stderr = subprocess.PIPE
 
199
    else:
 
200
        env = None
 
201
        stderr = None
 
202
 
 
203
    try:
 
204
        pipe = subprocess.Popen(diffcmd,
 
205
                                stdin=subprocess.PIPE,
 
206
                                stdout=subprocess.PIPE,
 
207
                                stderr=stderr,
 
208
                                env=env)
 
209
    except OSError as e:
 
210
        if e.errno == errno.ENOENT:
 
211
            raise errors.NoDiff(str(e))
 
212
        raise
 
213
 
 
214
    return pipe
 
215
 
 
216
 
 
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,
69
254
                  diff_opts):
70
255
    """Display a diff by calling out to the external diff program."""
71
 
    import sys
72
 
    
73
 
    if to_file != sys.stdout:
74
 
        raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
75
 
                                  to_file)
76
 
 
77
256
    # make sure our own output is properly ordered before the diff
78
257
    to_file.flush()
79
258
 
80
 
    from tempfile import NamedTemporaryFile
81
 
    import os
82
 
 
83
 
    oldtmpf = NamedTemporaryFile()
84
 
    newtmpf = NamedTemporaryFile()
 
259
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='brz-diff-old-')
 
260
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='brz-diff-new-')
 
261
    oldtmpf = os.fdopen(oldtmp_fd, 'wb')
 
262
    newtmpf = os.fdopen(newtmp_fd, 'wb')
85
263
 
86
264
    try:
87
265
        # TODO: perhaps a special case for comparing to or from the empty
94
272
        oldtmpf.writelines(oldlines)
95
273
        newtmpf.writelines(newlines)
96
274
 
97
 
        oldtmpf.flush()
98
 
        newtmpf.flush()
 
275
        oldtmpf.close()
 
276
        newtmpf.close()
99
277
 
100
278
        if not diff_opts:
101
279
            diff_opts = []
 
280
        if sys.platform == 'win32':
 
281
            # Popen doesn't do the proper encoding for external commands
 
282
            # 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')
102
285
        diffcmd = ['diff',
103
 
                   '--label', old_filename+'\t',
104
 
                   oldtmpf.name,
105
 
                   '--label', new_filename+'\t',
106
 
                   newtmpf.name]
107
 
 
108
 
        # diff only allows one style to be specified; they don't override.
109
 
        # note that some of these take optargs, and the optargs can be
110
 
        # directly appended to the options.
111
 
        # this is only an approximate parser; it doesn't properly understand
112
 
        # the grammar.
113
 
        for s in ['-c', '-u', '-C', '-U',
114
 
                  '-e', '--ed',
115
 
                  '-q', '--brief',
116
 
                  '--normal',
117
 
                  '-n', '--rcs',
118
 
                  '-y', '--side-by-side',
119
 
                  '-D', '--ifdef']:
120
 
            for j in diff_opts:
121
 
                if j.startswith(s):
122
 
                    break
123
 
            else:
124
 
                continue
125
 
            break
126
 
        else:
127
 
            diffcmd.append('-u')
128
 
                  
 
286
                   '--label', old_label,
 
287
                   old_abspath,
 
288
                   '--label', new_label,
 
289
                   new_abspath,
 
290
                   '--binary',
 
291
                   ]
 
292
 
 
293
        diff_opts = default_style_unified(diff_opts)
 
294
 
129
295
        if diff_opts:
130
296
            diffcmd.extend(diff_opts)
131
297
 
132
 
        rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
133
 
        
134
 
        if rc != 0 and rc != 1:
 
298
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
 
299
        out, err = pipe.communicate()
 
300
        rc = pipe.returncode
 
301
 
 
302
        # internal_diff() adds a trailing newline, add one here for consistency
 
303
        out += b'\n'
 
304
        if rc == 2:
 
305
            # 'diff' gives retcode == 2 for all sorts of errors
 
306
            # one of those is 'Binary files differ'.
 
307
            # Bad options could also be the problem.
 
308
            # 'Binary files' is not a real error, so we suppress that error.
 
309
            lang_c_out = out
 
310
 
 
311
            # Since we got here, we want to make sure to give an i18n error
 
312
            pipe = _spawn_external_diff(diffcmd, capture_errors=False)
 
313
            out, err = pipe.communicate()
 
314
 
 
315
            # Write out the new i18n diff response
 
316
            to_file.write(out + b'\n')
 
317
            if pipe.returncode != 2:
 
318
                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,))
 
322
 
 
323
            first_line = lang_c_out.split(b'\n', 1)[0]
 
324
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
 
325
            m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
 
326
            if m is None:
 
327
                raise errors.BzrError('external diff failed with exit code 2;'
 
328
                                      ' command: %r' % (diffcmd,))
 
329
            else:
 
330
                # Binary files differ, just return
 
331
                return
 
332
 
 
333
        # If we got to here, we haven't written out the output of diff
 
334
        # do so now
 
335
        to_file.write(out)
 
336
        if rc not in (0, 1):
135
337
            # returns 1 if files differ; that's OK
136
338
            if rc < 0:
137
339
                msg = 'signal %d' % (-rc)
138
340
            else:
139
341
                msg = 'exit code %d' % rc
140
 
                
141
 
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
 
342
 
 
343
            raise errors.BzrError('external diff failed with %s; command: %r'
 
344
                                  % (msg, diffcmd))
 
345
 
142
346
    finally:
143
347
        oldtmpf.close()                 # and delete
144
348
        newtmpf.close()
145
349
 
146
 
 
147
 
@deprecated_function(zero_eight)
148
 
def show_diff(b, from_spec, specific_files, external_diff_options=None,
149
 
              revision2=None, output=None, b2=None):
150
 
    """Shortcut for showing the diff to the working tree.
151
 
 
152
 
    Please use show_diff_trees instead.
153
 
 
154
 
    b
155
 
        Branch.
156
 
 
157
 
    revision
158
 
        None for 'basis tree', or otherwise the old revision to compare against.
159
 
    
160
 
    The more general form is show_diff_trees(), where the caller
161
 
    supplies any two trees.
 
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
 
 
363
 
 
364
def get_trees_and_branches_to_diff_locked(
 
365
        path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
 
366
    """Get the trees and specific files to diff given a list of paths.
 
367
 
 
368
    This method works out the trees to be diff'ed and the files of
 
369
    interest within those trees.
 
370
 
 
371
    :param path_list:
 
372
        the list of arguments passed to the diff command
 
373
    :param revision_specs:
 
374
        Zero, one or two RevisionSpecs from the diff command line,
 
375
        saying what revisions to compare.
 
376
    :param old_url:
 
377
        The url of the old branch or tree. If None, the tree to use is
 
378
        taken from the first path, if any, or the current working tree.
 
379
    :param new_url:
 
380
        The url of the new branch or tree. If None, the tree to use is
 
381
        taken from the first path, if any, or the current working tree.
 
382
    :param add_cleanup:
 
383
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
 
384
        will register cleanups that must be run to unlock the trees, etc.
 
385
    :param apply_view:
 
386
        if True and a view is set, apply the view or check that the paths
 
387
        are within it
 
388
    :returns:
 
389
        a tuple of (old_tree, new_tree, old_branch, new_branch,
 
390
        specific_files, extra_trees) where extra_trees is a sequence of
 
391
        additional trees to search in for file-ids.  The trees and branches
 
392
        will be read-locked until the cleanups registered via the add_cleanup
 
393
        param are run.
162
394
    """
163
 
    if output is None:
164
 
        import sys
165
 
        output = sys.stdout
 
395
    # Get the old and new revision specs
 
396
    old_revision_spec = None
 
397
    new_revision_spec = None
 
398
    if revision_specs is not None:
 
399
        if len(revision_specs) > 0:
 
400
            old_revision_spec = revision_specs[0]
 
401
            if old_url is None:
 
402
                old_url = old_revision_spec.get_branch()
 
403
        if len(revision_specs) > 1:
 
404
            new_revision_spec = revision_specs[1]
 
405
            if new_url is None:
 
406
                new_url = new_revision_spec.get_branch()
166
407
 
167
 
    if from_spec is None:
168
 
        old_tree = b.bzrdir.open_workingtree()
169
 
        if b2 is None:
170
 
            old_tree = old_tree = old_tree.basis_tree()
 
408
    other_paths = []
 
409
    make_paths_wt_relative = True
 
410
    consider_relpath = True
 
411
    if path_list is None or len(path_list) == 0:
 
412
        # If no path is given, the current working tree is used
 
413
        default_location = u'.'
 
414
        consider_relpath = False
 
415
    elif old_url is not None and new_url is not None:
 
416
        other_paths = path_list
 
417
        make_paths_wt_relative = False
171
418
    else:
172
 
        old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
173
 
 
174
 
    if revision2 is None:
175
 
        if b2 is None:
176
 
            new_tree = b.bzrdir.open_workingtree()
 
419
        default_location = path_list[0]
 
420
        other_paths = path_list[1:]
 
421
 
 
422
    def lock_tree_or_branch(wt, br):
 
423
        if wt is not None:
 
424
            wt.lock_read()
 
425
            add_cleanup(wt.unlock)
 
426
        elif br is not None:
 
427
            br.lock_read()
 
428
            add_cleanup(br.unlock)
 
429
 
 
430
    # Get the old location
 
431
    specific_files = []
 
432
    if old_url is None:
 
433
        old_url = default_location
 
434
    working_tree, branch, relpath = \
 
435
        controldir.ControlDir.open_containing_tree_or_branch(old_url)
 
436
    lock_tree_or_branch(working_tree, branch)
 
437
    if consider_relpath and relpath != '':
 
438
        if working_tree is not None and apply_view:
 
439
            views.check_path_in_view(working_tree, relpath)
 
440
        specific_files.append(relpath)
 
441
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
 
442
    old_branch = branch
 
443
 
 
444
    # Get the new location
 
445
    if new_url is None:
 
446
        new_url = default_location
 
447
    if new_url != old_url:
 
448
        working_tree, branch, relpath = \
 
449
            controldir.ControlDir.open_containing_tree_or_branch(new_url)
 
450
        lock_tree_or_branch(working_tree, branch)
 
451
        if consider_relpath and relpath != '':
 
452
            if working_tree is not None and apply_view:
 
453
                views.check_path_in_view(working_tree, relpath)
 
454
            specific_files.append(relpath)
 
455
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
 
456
                                 basis_is_default=working_tree is None)
 
457
    new_branch = branch
 
458
 
 
459
    # Get the specific files (all files is None, no files is [])
 
460
    if make_paths_wt_relative and working_tree is not None:
 
461
        other_paths = working_tree.safe_relpath_files(
 
462
            other_paths,
 
463
            apply_view=apply_view)
 
464
    specific_files.extend(other_paths)
 
465
    if len(specific_files) == 0:
 
466
        specific_files = None
 
467
        if (working_tree is not None and working_tree.supports_views() and
 
468
                apply_view):
 
469
            view_files = working_tree.views.lookup_view()
 
470
            if view_files:
 
471
                specific_files = view_files
 
472
                view_str = views.view_display_str(view_files)
 
473
                note(gettext("*** Ignoring files outside view. View is %s") % view_str)
 
474
 
 
475
    # Get extra trees that ought to be searched for file-ids
 
476
    extra_trees = None
 
477
    if working_tree is not None and working_tree not in (old_tree, new_tree):
 
478
        extra_trees = (working_tree,)
 
479
    return (old_tree, new_tree, old_branch, new_branch,
 
480
            specific_files, extra_trees)
 
481
 
 
482
 
 
483
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
 
484
    if branch is None and tree is not None:
 
485
        branch = tree.branch
 
486
    if spec is None or spec.spec is None:
 
487
        if basis_is_default:
 
488
            if tree is not None:
 
489
                return tree.basis_tree()
 
490
            else:
 
491
                return branch.basis_tree()
177
492
        else:
178
 
            new_tree = b2.bzrdir.open_workingtree()
179
 
    else:
180
 
        new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
181
 
 
182
 
    return show_diff_trees(old_tree, new_tree, output, specific_files,
183
 
                           external_diff_options)
184
 
 
185
 
 
186
 
def diff_cmd_helper(tree, specific_files, external_diff_options, 
187
 
                    old_revision_spec=None, new_revision_spec=None):
188
 
    """Helper for cmd_diff.
189
 
 
190
 
   tree 
191
 
        A WorkingTree
192
 
 
193
 
    specific_files
194
 
        The specific files to compare, or None
195
 
 
196
 
    external_diff_options
197
 
        If non-None, run an external diff, and pass it these options
198
 
 
199
 
    old_revision_spec
200
 
        If None, use basis tree as old revision, otherwise use the tree for
201
 
        the specified revision. 
202
 
 
203
 
    new_revision_spec
204
 
        If None, use working tree as new revision, otherwise use the tree for
205
 
        the specified revision.
206
 
    
207
 
    The more general form is show_diff_trees(), where the caller
208
 
    supplies any two trees.
209
 
    """
210
 
    import sys
211
 
    output = sys.stdout
212
 
    def spec_tree(spec):
213
 
        revision_id = spec.in_store(tree.branch).rev_id
214
 
        return tree.branch.repository.revision_tree(revision_id)
215
 
    if old_revision_spec is None:
216
 
        old_tree = tree.basis_tree()
217
 
    else:
218
 
        old_tree = spec_tree(old_revision_spec)
219
 
 
220
 
    if new_revision_spec is None:
221
 
        new_tree = tree
222
 
    else:
223
 
        new_tree = spec_tree(new_revision_spec)
224
 
 
225
 
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
226
 
                           external_diff_options)
 
493
            return tree
 
494
    return spec.as_tree(branch)
227
495
 
228
496
 
229
497
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
230
 
                    external_diff_options=None):
 
498
                    external_diff_options=None,
 
499
                    old_label='a/', new_label='b/',
 
500
                    extra_trees=None,
 
501
                    path_encoding='utf8',
 
502
                    using=None,
 
503
                    format_cls=None,
 
504
                    context=DEFAULT_CONTEXT_AMOUNT):
231
505
    """Show in text form the changes from one tree to another.
232
506
 
233
 
    to_files
234
 
        If set, include only changes to these files.
235
 
 
236
 
    external_diff_options
237
 
        If set, use an external GNU diff and pass these options.
 
507
    :param to_file: The output stream.
 
508
    :param specific_files: Include only changes to these files - None for all
 
509
        changes.
 
510
    :param external_diff_options: If set, use an external GNU diff and pass
 
511
        these options.
 
512
    :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,
 
514
        otherwise is supposed to be utf8
 
515
    :param format_cls: Formatter class (DiffTree subclass)
238
516
    """
239
 
    old_tree.lock_read()
240
 
    try:
 
517
    if context is None:
 
518
        context = DEFAULT_CONTEXT_AMOUNT
 
519
    if format_cls is None:
 
520
        format_cls = DiffTree
 
521
    with old_tree.lock_read():
 
522
        if extra_trees is not None:
 
523
            for tree in extra_trees:
 
524
                tree.lock_read()
241
525
        new_tree.lock_read()
242
526
        try:
243
 
            return _show_diff_trees(old_tree, new_tree, to_file,
244
 
                                    specific_files, external_diff_options)
 
527
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
528
                                                   path_encoding,
 
529
                                                   external_diff_options,
 
530
                                                   old_label, new_label, using,
 
531
                                                   context_lines=context)
 
532
            return differ.show_diff(specific_files, extra_trees)
245
533
        finally:
246
534
            new_tree.unlock()
247
 
    finally:
248
 
        old_tree.unlock()
249
 
 
250
 
 
251
 
def _show_diff_trees(old_tree, new_tree, to_file,
252
 
                     specific_files, external_diff_options):
253
 
 
254
 
    # TODO: Options to control putting on a prefix or suffix, perhaps
255
 
    # as a format string?
256
 
    old_label = 'a/'
257
 
    new_label = 'b/'
258
 
 
259
 
    DEVNULL = '/dev/null'
260
 
    # Windows users, don't panic about this filename -- it is a
261
 
    # special signal to GNU patch that the file should be created or
262
 
    # deleted respectively.
263
 
 
264
 
    # TODO: Generation of pseudo-diffs for added/deleted files could
265
 
    # be usefully made into a much faster special case.
266
 
 
267
 
    _raise_if_doubly_unversioned(specific_files, old_tree, new_tree)
268
 
 
269
 
    if external_diff_options:
270
 
        assert isinstance(external_diff_options, basestring)
271
 
        opts = external_diff_options.split()
272
 
        def diff_file(olab, olines, nlab, nlines, to_file):
273
 
            external_diff(olab, olines, nlab, nlines, to_file, opts)
274
 
    else:
275
 
        diff_file = internal_diff
276
 
    
277
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
278
 
                          specific_files=specific_files)
279
 
 
280
 
    has_changes = 0
281
 
    for path, file_id, kind in delta.removed:
282
 
        has_changes = 1
283
 
        print >>to_file, '=== removed %s %r' % (kind, old_label + path)
284
 
        old_tree.inventory[file_id].diff(diff_file, old_label + path, old_tree,
285
 
                                         DEVNULL, None, None, to_file)
286
 
    for path, file_id, kind in delta.added:
287
 
        has_changes = 1
288
 
        print >>to_file, '=== added %s %r' % (kind, new_label + path)
289
 
        new_tree.inventory[file_id].diff(diff_file, new_label + path, new_tree,
290
 
                                         DEVNULL, None, None, to_file, 
291
 
                                         reverse=True)
292
 
    for (old_path, new_path, file_id, kind,
293
 
         text_modified, meta_modified) in delta.renamed:
294
 
        has_changes = 1
295
 
        prop_str = get_prop_change(meta_modified)
296
 
        print >>to_file, '=== renamed %s %r => %r%s' % (
297
 
                    kind, old_label + old_path, new_label + new_path, prop_str)
298
 
        _maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
299
 
                                    new_label, new_path, new_tree,
300
 
                                    text_modified, kind, to_file, diff_file)
301
 
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
302
 
        has_changes = 1
303
 
        prop_str = get_prop_change(meta_modified)
304
 
        print >>to_file, '=== modified %s %r%s' % (kind, old_label + path,
305
 
                    prop_str)
306
 
        if text_modified:
307
 
            _maybe_diff_file_or_symlink(old_label, path, old_tree, file_id,
308
 
                                        new_label, path, new_tree,
309
 
                                        True, kind, to_file, diff_file)
310
 
 
311
 
    return has_changes
312
 
 
313
 
 
314
 
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
315
 
    """Complain if paths are not versioned in either tree."""
316
 
    if not specific_files:
317
 
        return
318
 
    old_unversioned = old_tree.filter_unversioned_files(specific_files)
319
 
    new_unversioned = new_tree.filter_unversioned_files(specific_files)
320
 
    unversioned = old_unversioned.intersection(new_unversioned)
321
 
    if unversioned:
322
 
        raise errors.PathsNotVersionedError(sorted(unversioned))
323
 
    
324
 
 
325
 
def get_prop_change(meta_modified):
326
 
    if meta_modified:
327
 
        return " (properties changed)"
328
 
    else:
329
 
        return  ""
330
 
 
331
 
 
332
 
def _maybe_diff_file_or_symlink(old_label, old_path, old_tree, file_id,
333
 
                                new_label, new_path, new_tree, text_modified,
334
 
                                kind, to_file, diff_file):
335
 
    if text_modified:
336
 
        new_entry = new_tree.inventory[file_id]
337
 
        old_tree.inventory[file_id].diff(diff_file,
338
 
                                         old_label + old_path, old_tree,
339
 
                                         new_label + new_path, new_entry, 
340
 
                                         new_tree, to_file)
 
535
            if extra_trees is not None:
 
536
                for tree in extra_trees:
 
537
                    tree.unlock()
 
538
 
 
539
 
 
540
def _patch_header_date(tree, path):
 
541
    """Returns a timestamp suitable for use in a patch header."""
 
542
    try:
 
543
        mtime = tree.get_file_mtime(path)
 
544
    except FileTimestampUnavailable:
 
545
        mtime = 0
 
546
    return timestamp.format_patch_date(mtime)
 
547
 
 
548
 
 
549
def get_executable_change(old_is_x, new_is_x):
 
550
    descr = {True: b"+x", False: b"-x", None: b"??"}
 
551
    if old_is_x != new_is_x:
 
552
        return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
 
553
    else:
 
554
        return []
 
555
 
 
556
 
 
557
class DiffPath(object):
 
558
    """Base type for command object that compare files"""
 
559
 
 
560
    # The type or contents of the file were unsuitable for diffing
 
561
    CANNOT_DIFF = 'CANNOT_DIFF'
 
562
    # The file has changed in a semantic way
 
563
    CHANGED = 'CHANGED'
 
564
    # The file content may have changed, but there is no semantic change
 
565
    UNCHANGED = 'UNCHANGED'
 
566
 
 
567
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
 
568
        """Constructor.
 
569
 
 
570
        :param old_tree: The tree to show as the old tree in the comparison
 
571
        :param new_tree: The tree to show as new in the comparison
 
572
        :param to_file: The file to write comparison data to
 
573
        :param path_encoding: The character encoding to write paths in
 
574
        """
 
575
        self.old_tree = old_tree
 
576
        self.new_tree = new_tree
 
577
        self.to_file = to_file
 
578
        self.path_encoding = path_encoding
 
579
 
 
580
    def finish(self):
 
581
        pass
 
582
 
 
583
    @classmethod
 
584
    def from_diff_tree(klass, diff_tree):
 
585
        return klass(diff_tree.old_tree, diff_tree.new_tree,
 
586
                     diff_tree.to_file, diff_tree.path_encoding)
 
587
 
 
588
    @staticmethod
 
589
    def _diff_many(differs, old_path, new_path, old_kind, new_kind):
 
590
        for file_differ in differs:
 
591
            result = file_differ.diff(old_path, new_path, old_kind, new_kind)
 
592
            if result is not DiffPath.CANNOT_DIFF:
 
593
                return result
 
594
        else:
 
595
            return DiffPath.CANNOT_DIFF
 
596
 
 
597
 
 
598
class DiffKindChange(object):
 
599
    """Special differ for file kind changes.
 
600
 
 
601
    Represents kind change as deletion + creation.  Uses the other differs
 
602
    to do this.
 
603
    """
 
604
 
 
605
    def __init__(self, differs):
 
606
        self.differs = differs
 
607
 
 
608
    def finish(self):
 
609
        pass
 
610
 
 
611
    @classmethod
 
612
    def from_diff_tree(klass, diff_tree):
 
613
        return klass(diff_tree.differs)
 
614
 
 
615
    def diff(self, old_path, new_path, old_kind, new_kind):
 
616
        """Perform comparison
 
617
 
 
618
        :param old_path: Path of the file in the old tree
 
619
        :param new_path: Path of the file in the new tree
 
620
        :param old_kind: Old file-kind of the file
 
621
        :param new_kind: New file-kind of the file
 
622
        """
 
623
        if None in (old_kind, new_kind):
 
624
            return DiffPath.CANNOT_DIFF
 
625
        result = DiffPath._diff_many(
 
626
            self.differs, old_path, new_path, old_kind, None)
 
627
        if result is DiffPath.CANNOT_DIFF:
 
628
            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
 
646
 
 
647
 
 
648
class DiffDirectory(DiffPath):
 
649
 
 
650
    def diff(self, old_path, new_path, old_kind, new_kind):
 
651
        """Perform comparison between two directories.  (dummy)
 
652
 
 
653
        """
 
654
        if 'directory' not in (old_kind, new_kind):
 
655
            return self.CANNOT_DIFF
 
656
        if old_kind not in ('directory', None):
 
657
            return self.CANNOT_DIFF
 
658
        if new_kind not in ('directory', None):
 
659
            return self.CANNOT_DIFF
 
660
        return self.CHANGED
 
661
 
 
662
 
 
663
class DiffSymlink(DiffPath):
 
664
 
 
665
    def diff(self, old_path, new_path, old_kind, new_kind):
 
666
        """Perform comparison between two symlinks
 
667
 
 
668
        :param old_path: Path of the file in the old tree
 
669
        :param new_path: Path of the file in the new tree
 
670
        :param old_kind: Old file-kind of the file
 
671
        :param new_kind: New file-kind of the file
 
672
        """
 
673
        if 'symlink' not in (old_kind, new_kind):
 
674
            return self.CANNOT_DIFF
 
675
        if old_kind == 'symlink':
 
676
            old_target = self.old_tree.get_symlink_target(old_path)
 
677
        elif old_kind is None:
 
678
            old_target = None
 
679
        else:
 
680
            return self.CANNOT_DIFF
 
681
        if new_kind == 'symlink':
 
682
            new_target = self.new_tree.get_symlink_target(new_path)
 
683
        elif new_kind is None:
 
684
            new_target = None
 
685
        else:
 
686
            return self.CANNOT_DIFF
 
687
        return self.diff_symlink(old_target, new_target)
 
688
 
 
689
    def diff_symlink(self, old_target, new_target):
 
690
        if old_target is None:
 
691
            self.to_file.write(b'=== target is \'%s\'\n' %
 
692
                               new_target.encode(self.path_encoding, 'replace'))
 
693
        elif new_target is None:
 
694
            self.to_file.write(b'=== target was \'%s\'\n' %
 
695
                               old_target.encode(self.path_encoding, 'replace'))
 
696
        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')))
 
700
        return self.CHANGED
 
701
 
 
702
 
 
703
class DiffText(DiffPath):
 
704
 
 
705
    # GNU Patch uses the epoch date to detect files that are being added
 
706
    # or removed in a diff.
 
707
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
 
708
 
 
709
    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):
 
712
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
713
        self.text_differ = text_differ
 
714
        self.old_label = old_label
 
715
        self.new_label = new_label
 
716
        self.path_encoding = path_encoding
 
717
        self.context_lines = context_lines
 
718
 
 
719
    def diff(self, old_path, new_path, old_kind, new_kind):
 
720
        """Compare two files in unified diff format
 
721
 
 
722
        :param old_path: Path of the file in the old tree
 
723
        :param new_path: Path of the file in the new tree
 
724
        :param old_kind: Old file-kind of the file
 
725
        :param new_kind: New file-kind of the file
 
726
        """
 
727
        if 'file' not in (old_kind, new_kind):
 
728
            return self.CANNOT_DIFF
 
729
        if old_kind == 'file':
 
730
            old_date = _patch_header_date(self.old_tree, old_path)
 
731
        elif old_kind is None:
 
732
            old_date = self.EPOCH_DATE
 
733
        else:
 
734
            return self.CANNOT_DIFF
 
735
        if new_kind == 'file':
 
736
            new_date = _patch_header_date(self.new_tree, new_path)
 
737
        elif new_kind is None:
 
738
            new_date = self.EPOCH_DATE
 
739
        else:
 
740
            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)
 
746
 
 
747
    def diff_text(self, from_path, to_path, from_label, to_label):
 
748
        """Diff the content of given files in two trees
 
749
 
 
750
        :param from_path: The path in the from tree. If None,
 
751
            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,
 
754
            the file is not present in the to tree.
 
755
        """
 
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:
 
762
                return []
 
763
        try:
 
764
            from_text = _get_text(self.old_tree, from_path)
 
765
            to_text = _get_text(self.new_tree, to_path)
 
766
            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)
 
769
        except errors.BinaryFile:
 
770
            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'))
 
773
        return self.CHANGED
 
774
 
 
775
 
 
776
class DiffFromTool(DiffPath):
 
777
 
 
778
    def __init__(self, command_template, old_tree, new_tree, to_file,
 
779
                 path_encoding='utf-8'):
 
780
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
 
781
        self.command_template = command_template
 
782
        self._root = osutils.mkdtemp(prefix='brz-diff-')
 
783
 
 
784
    @classmethod
 
785
    def from_string(klass, command_template, old_tree, new_tree, to_file,
 
786
                    path_encoding='utf-8'):
 
787
        return klass(command_template, old_tree, new_tree, to_file,
 
788
                     path_encoding)
 
789
 
 
790
    @classmethod
 
791
    def make_from_diff_tree(klass, command_string, external_diff_options=None):
 
792
        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,
 
797
                                     diff_tree.new_tree, diff_tree.to_file)
 
798
        return from_diff_tree
 
799
 
 
800
    def _get_command(self, old_path, new_path):
 
801
        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
 
814
 
 
815
    def _execute(self, old_path, new_path):
 
816
        command = self._get_command(old_path, new_path)
 
817
        try:
 
818
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
 
819
                                    cwd=self._root)
 
820
        except OSError as e:
 
821
            if e.errno == errno.ENOENT:
 
822
                raise errors.ExecutableMissing(command[0])
 
823
            else:
 
824
                raise
 
825
        self.to_file.write(proc.stdout.read())
 
826
        proc.stdout.close()
 
827
        return proc.wait()
 
828
 
 
829
    def _try_symlink_root(self, tree, prefix):
 
830
        if (getattr(tree, 'abspath', None) is None or
 
831
                not osutils.host_os_dereferences_symlinks()):
 
832
            return False
 
833
        try:
 
834
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
 
835
        except OSError as e:
 
836
            if e.errno != errno.EEXIST:
 
837
                raise
 
838
        return True
 
839
 
 
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,
 
869
                    allow_write=False):
 
870
        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)
 
876
        if not force_temp and self._try_symlink_root(tree, prefix):
 
877
            return full_path
 
878
        parent_dir = osutils.dirname(full_path)
 
879
        try:
 
880
            os.makedirs(parent_dir)
 
881
        except OSError as e:
 
882
            if e.errno != errno.EEXIST:
 
883
                raise
 
884
        source = tree.get_file(relpath)
 
885
        try:
 
886
            with open(full_path, 'wb') as target:
 
887
                osutils.pumpfile(source, target)
 
888
        finally:
 
889
            source.close()
 
890
        try:
 
891
            mtime = tree.get_file_mtime(relpath)
 
892
        except FileTimestampUnavailable:
 
893
            pass
 
894
        else:
 
895
            os.utime(full_path, (mtime, mtime))
 
896
        if not allow_write:
 
897
            osutils.make_readonly(full_path)
 
898
        return full_path
 
899
 
 
900
    def _prepare_files(self, old_path, new_path, force_temp=False,
 
901
                       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)
 
907
        return old_disk_path, new_disk_path
 
908
 
 
909
    def finish(self):
 
910
        try:
 
911
            osutils.rmtree(self._root)
 
912
        except OSError as e:
 
913
            if e.errno != errno.ENOENT:
 
914
                mutter("The temporary directory \"%s\" was not "
 
915
                       "cleanly removed: %s." % (self._root, e))
 
916
 
 
917
    def diff(self, old_path, new_path, old_kind, new_kind):
 
918
        if (old_kind, new_kind) != ('file', 'file'):
 
919
            return DiffPath.CANNOT_DIFF
 
920
        (old_disk_path, new_disk_path) = self._prepare_files(
 
921
            old_path, new_path)
 
922
        self._execute(old_disk_path, new_disk_path)
 
923
 
 
924
    def edit_file(self, old_path, new_path):
 
925
        """Use this tool to edit a file.
 
926
 
 
927
        A temporary copy will be edited, and the new contents will be
 
928
        returned.
 
929
 
 
930
        :return: The new contents of the file.
 
931
        """
 
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)
 
935
        subprocess.call(command, cwd=self._root)
 
936
        with open(new_abs_path, 'rb') as new_file:
 
937
            return new_file.read()
 
938
 
 
939
 
 
940
class DiffTree(object):
 
941
    """Provides textual representations of the difference between two trees.
 
942
 
 
943
    A DiffTree examines two trees and where a file-id has altered
 
944
    between them, generates a textual representation of the difference.
 
945
    DiffTree uses a sequence of DiffPath objects which are each
 
946
    given the opportunity to handle a given altered fileid. The list
 
947
    of DiffPath objects can be extended globally by appending to
 
948
    DiffTree.diff_factories, or for a specific diff operation by
 
949
    supplying the extra_factories option to the appropriate method.
 
950
    """
 
951
 
 
952
    # list of factories that can provide instances of DiffPath objects
 
953
    # may be extended by plugins.
 
954
    diff_factories = [DiffSymlink.from_diff_tree,
 
955
                      DiffDirectory.from_diff_tree,
 
956
                      DiffTreeReference.from_diff_tree]
 
957
 
 
958
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
 
959
                 diff_text=None, extra_factories=None):
 
960
        """Constructor
 
961
 
 
962
        :param old_tree: Tree to show as old in the comparison
 
963
        :param new_tree: Tree to show as new in the comparison
 
964
        :param to_file: File to write comparision to
 
965
        :param path_encoding: Character encoding to write paths in
 
966
        :param diff_text: DiffPath-type object to use as a last resort for
 
967
            diffing text files.
 
968
        :param extra_factories: Factories of DiffPaths to try before any other
 
969
            DiffPaths"""
 
970
        if diff_text is None:
 
971
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
972
                                 '', '', internal_diff)
 
973
        self.old_tree = old_tree
 
974
        self.new_tree = new_tree
 
975
        self.to_file = to_file
 
976
        self.path_encoding = path_encoding
 
977
        self.differs = []
 
978
        if extra_factories is not None:
 
979
            self.differs.extend(f(self) for f in extra_factories)
 
980
        self.differs.extend(f(self) for f in self.diff_factories)
 
981
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
 
982
 
 
983
    @classmethod
 
984
    def from_trees_options(klass, old_tree, new_tree, to_file,
 
985
                           path_encoding, external_diff_options, old_label,
 
986
                           new_label, using, context_lines):
 
987
        """Factory for producing a DiffTree.
 
988
 
 
989
        Designed to accept options used by show_diff_trees.
 
990
 
 
991
        :param old_tree: The tree to show as old in the comparison
 
992
        :param new_tree: The tree to show as new in the comparison
 
993
        :param to_file: File to write comparisons to
 
994
        :param path_encoding: Character encoding to use for writing paths
 
995
        :param external_diff_options: If supplied, use the installed diff
 
996
            binary to perform file comparison, using supplied options.
 
997
        :param old_label: Prefix to use for old file labels
 
998
        :param new_label: Prefix to use for new file labels
 
999
        :param using: Commandline to use to invoke an external diff tool
 
1000
        """
 
1001
        if using is not None:
 
1002
            extra_factories = [DiffFromTool.make_from_diff_tree(
 
1003
                using, external_diff_options)]
 
1004
        else:
 
1005
            extra_factories = []
 
1006
        if external_diff_options:
 
1007
            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
                """
 
1013
                external_diff(olab, olines, nlab, nlines, to_file, opts)
 
1014
        else:
 
1015
            diff_file = internal_diff
 
1016
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
 
1017
                             old_label, new_label, diff_file, context_lines=context_lines)
 
1018
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
 
1019
                     extra_factories)
 
1020
 
 
1021
    def show_diff(self, specific_files, extra_trees=None):
 
1022
        """Write tree diff to self.to_file
 
1023
 
 
1024
        :param specific_files: the specific files to compare (recursive)
 
1025
        :param extra_trees: extra trees to use for mapping paths to file_ids
 
1026
        """
 
1027
        try:
 
1028
            return self._show_diff(specific_files, extra_trees)
 
1029
        finally:
 
1030
            for differ in self.differs:
 
1031
                differ.finish()
 
1032
 
 
1033
    def _show_diff(self, specific_files, extra_trees):
 
1034
        # TODO: Generation of pseudo-diffs for added/deleted files could
 
1035
        # be usefully made into a much faster special case.
 
1036
        iterator = self.new_tree.iter_changes(self.old_tree,
 
1037
                                              specific_files=specific_files,
 
1038
                                              extra_trees=extra_trees,
 
1039
                                              require_versioned=True)
 
1040
        has_changes = 0
 
1041
 
 
1042
        def changes_key(change):
 
1043
            old_path, new_path = change[1]
 
1044
            path = new_path
 
1045
            if path is None:
 
1046
                path = old_path
 
1047
            return path
 
1048
 
 
1049
        def get_encoded_path(path):
 
1050
            if path is not None:
 
1051
                return path.encode(self.path_encoding, "replace")
 
1052
        for change in sorted(iterator, key=changes_key):
 
1053
            # The root does not get diffed, and items with no known kind (that
 
1054
            # 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])
 
1070
 
 
1071
            properties_changed = []
 
1072
            properties_changed.extend(
 
1073
                get_executable_change(executable[0], executable[1]))
 
1074
 
 
1075
            if properties_changed:
 
1076
                prop_str = b" (properties changed: %s)" % (
 
1077
                    b", ".join(properties_changed),)
 
1078
            else:
 
1079
                prop_str = b""
 
1080
 
 
1081
            if (old_present, new_present) == (True, False):
 
1082
                self.to_file.write(b"=== removed %s '%s'\n" %
 
1083
                                   (kind[0].encode('ascii'), oldpath_encoded))
 
1084
                newpath = oldpath
 
1085
            elif (old_present, new_present) == (False, True):
 
1086
                self.to_file.write(b"=== added %s '%s'\n" %
 
1087
                                   (kind[1].encode('ascii'), newpath_encoded))
 
1088
                oldpath = newpath
 
1089
            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))
 
1092
            else:
 
1093
                # if it was produced by iter_changes, it must be
 
1094
                # 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])
 
1099
                has_changes = 1
 
1100
            if renamed:
 
1101
                has_changes = 1
 
1102
        return has_changes
 
1103
 
 
1104
    def diff(self, old_path, new_path):
 
1105
        """Perform a diff of a single file
 
1106
 
 
1107
        :param old_path: The path of the file in the old tree
 
1108
        :param new_path: The path of the file in the new tree
 
1109
        """
 
1110
        if old_path is None:
 
1111
            old_kind = None
 
1112
        else:
 
1113
            old_kind = self.old_tree.kind(old_path)
 
1114
        if new_path is None:
 
1115
            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)
 
1123
        if result is DiffPath.CANNOT_DIFF:
 
1124
            error_path = new_path
 
1125
            if error_path is None:
 
1126
                error_path = old_path
 
1127
            raise errors.NoDiffFound(error_path)
 
1128
 
 
1129
 
 
1130
format_registry = Registry()
 
1131
format_registry.register('default', DiffTree)