/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/diff.py

  • Committer: Jelmer Vernooij
  • Date: 2018-11-03 22:40:55 UTC
  • mto: This revision was merged to the branch mainline in revision 7157.
  • Revision ID: jelmer@jelmer.uk-20181103224055-wpa8tyxvmo5t60dy
Implement TreeReference.kind_character.

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