/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-06-30 16:06:19 UTC
  • mfrom: (5971.1.80 bzr-gpgme)
  • Revision ID: pqm@pqm.ubuntu.com-20110630160619-3022zmfchft893nt
(jr) A new command ``bzr verify-signatures`` has been added to check that
 commits
 are correctly signed with trusted keys by GPG. This requires python-gpgme to
 be installed. ``bzr log`` has gained a ``--signatures`` option to list the
 validity of signatures for each commit. New config options
 ``acceptable_keys``
 and ``validate_signatures_in_log`` can be set to control options to these
 commands. (Jonathan Riddell)

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