/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

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
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
 
import difflib
18
 
import os
19
 
import re
20
 
import sys
21
 
 
22
 
from bzrlib.lazy_import import lazy_import
23
 
lazy_import(globals(), """
24
 
import errno
25
 
import subprocess
26
 
import tempfile
27
 
import time
28
 
 
29
 
from bzrlib import (
30
 
    bzrdir,
31
 
    errors,
32
 
    osutils,
33
 
    patiencediff,
34
 
    textfile,
35
 
    timestamp,
36
 
    )
37
 
""")
38
 
 
39
 
from bzrlib.symbol_versioning import (
40
 
        deprecated_function,
41
 
        zero_ninetythree,
42
 
        )
43
 
from bzrlib.trace import mutter, warning
44
 
 
45
 
 
46
 
# TODO: Rather than building a changeset object, we should probably
47
 
# invoke callbacks on an object.  That object can either accumulate a
48
 
# list, write them out directly, etc etc.
49
 
 
50
 
 
51
 
class _PrematchedMatcher(difflib.SequenceMatcher):
52
 
    """Allow SequenceMatcher operations to use predetermined blocks"""
53
 
 
54
 
    def __init__(self, matching_blocks):
55
 
        difflib.SequenceMatcher(self, None, None)
56
 
        self.matching_blocks = matching_blocks
57
 
        self.opcodes = None
58
 
 
59
 
 
60
 
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
61
 
                  allow_binary=False, sequence_matcher=None,
62
 
                  path_encoding='utf8'):
63
 
    # FIXME: difflib is wrong if there is no trailing newline.
64
 
    # The syntax used by patch seems to be "\ No newline at
65
 
    # end of file" following the last diff line from that
66
 
    # file.  This is not trivial to insert into the
67
 
    # unified_diff output and it might be better to just fix
68
 
    # or replace that function.
69
 
 
70
 
    # In the meantime we at least make sure the patch isn't
71
 
    # mangled.
72
 
 
73
 
 
74
 
    # Special workaround for Python2.3, where difflib fails if
75
 
    # both sequences are empty.
76
 
    if not oldlines and not newlines:
77
 
        return
78
 
    
79
 
    if allow_binary is False:
80
 
        textfile.check_text_lines(oldlines)
81
 
        textfile.check_text_lines(newlines)
82
 
 
83
 
    if sequence_matcher is None:
84
 
        sequence_matcher = patiencediff.PatienceSequenceMatcher
85
 
    ud = patiencediff.unified_diff(oldlines, newlines,
86
 
                      fromfile=old_filename.encode(path_encoding),
87
 
                      tofile=new_filename.encode(path_encoding),
88
 
                      sequencematcher=sequence_matcher)
89
 
 
90
 
    ud = list(ud)
91
 
    if len(ud) == 0: # Identical contents, nothing to do
92
 
        return
93
 
    # work-around for difflib being too smart for its own good
94
 
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
95
 
    if not oldlines:
96
 
        ud[2] = ud[2].replace('-1,0', '-0,0')
97
 
    elif not newlines:
98
 
        ud[2] = ud[2].replace('+1,0', '+0,0')
99
 
    # work around for difflib emitting random spaces after the label
100
 
    ud[0] = ud[0][:-2] + '\n'
101
 
    ud[1] = ud[1][:-2] + '\n'
102
 
 
103
 
    for line in ud:
104
 
        to_file.write(line)
105
 
        if not line.endswith('\n'):
106
 
            to_file.write("\n\\ No newline at end of file\n")
107
 
    to_file.write('\n')
108
 
 
109
 
 
110
 
def _spawn_external_diff(diffcmd, capture_errors=True):
111
 
    """Spawn the externall diff process, and return the child handle.
112
 
 
113
 
    :param diffcmd: The command list to spawn
114
 
    :param capture_errors: Capture stderr as well as setting LANG=C
115
 
        and LC_ALL=C. This lets us read and understand the output of diff,
116
 
        and respond to any errors.
117
 
    :return: A Popen object.
118
 
    """
119
 
    if capture_errors:
120
 
        # construct minimal environment
121
 
        env = {}
122
 
        path = os.environ.get('PATH')
123
 
        if path is not None:
124
 
            env['PATH'] = path
125
 
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
126
 
        env['LANG'] = 'C'
127
 
        env['LC_ALL'] = 'C'
128
 
        stderr = subprocess.PIPE
129
 
    else:
130
 
        env = None
131
 
        stderr = None
132
 
 
133
 
    try:
134
 
        pipe = subprocess.Popen(diffcmd,
135
 
                                stdin=subprocess.PIPE,
136
 
                                stdout=subprocess.PIPE,
137
 
                                stderr=stderr,
138
 
                                env=env)
139
 
    except OSError, e:
140
 
        if e.errno == errno.ENOENT:
141
 
            raise errors.NoDiff(str(e))
142
 
        raise
143
 
 
144
 
    return pipe
145
 
 
146
 
 
147
 
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
148
 
                  diff_opts):
149
 
    """Display a diff by calling out to the external diff program."""
150
 
    # make sure our own output is properly ordered before the diff
151
 
    to_file.flush()
152
 
 
153
 
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
154
 
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
155
 
    oldtmpf = os.fdopen(oldtmp_fd, 'wb')
156
 
    newtmpf = os.fdopen(newtmp_fd, 'wb')
157
 
 
158
 
    try:
159
 
        # TODO: perhaps a special case for comparing to or from the empty
160
 
        # sequence; can just use /dev/null on Unix
161
 
 
162
 
        # TODO: if either of the files being compared already exists as a
163
 
        # regular named file (e.g. in the working directory) then we can
164
 
        # compare directly to that, rather than copying it.
165
 
 
166
 
        oldtmpf.writelines(oldlines)
167
 
        newtmpf.writelines(newlines)
168
 
 
169
 
        oldtmpf.close()
170
 
        newtmpf.close()
171
 
 
172
 
        if not diff_opts:
173
 
            diff_opts = []
174
 
        diffcmd = ['diff',
175
 
                   '--label', old_filename,
176
 
                   old_abspath,
177
 
                   '--label', new_filename,
178
 
                   new_abspath,
179
 
                   '--binary',
180
 
                  ]
181
 
 
182
 
        # diff only allows one style to be specified; they don't override.
183
 
        # note that some of these take optargs, and the optargs can be
184
 
        # directly appended to the options.
185
 
        # this is only an approximate parser; it doesn't properly understand
186
 
        # the grammar.
187
 
        for s in ['-c', '-u', '-C', '-U',
188
 
                  '-e', '--ed',
189
 
                  '-q', '--brief',
190
 
                  '--normal',
191
 
                  '-n', '--rcs',
192
 
                  '-y', '--side-by-side',
193
 
                  '-D', '--ifdef']:
194
 
            for j in diff_opts:
195
 
                if j.startswith(s):
196
 
                    break
197
 
            else:
198
 
                continue
199
 
            break
200
 
        else:
201
 
            diffcmd.append('-u')
202
 
                  
203
 
        if diff_opts:
204
 
            diffcmd.extend(diff_opts)
205
 
 
206
 
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
207
 
        out,err = pipe.communicate()
208
 
        rc = pipe.returncode
209
 
        
210
 
        # internal_diff() adds a trailing newline, add one here for consistency
211
 
        out += '\n'
212
 
        if rc == 2:
213
 
            # 'diff' gives retcode == 2 for all sorts of errors
214
 
            # one of those is 'Binary files differ'.
215
 
            # Bad options could also be the problem.
216
 
            # 'Binary files' is not a real error, so we suppress that error.
217
 
            lang_c_out = out
218
 
 
219
 
            # Since we got here, we want to make sure to give an i18n error
220
 
            pipe = _spawn_external_diff(diffcmd, capture_errors=False)
221
 
            out, err = pipe.communicate()
222
 
 
223
 
            # Write out the new i18n diff response
224
 
            to_file.write(out+'\n')
225
 
            if pipe.returncode != 2:
226
 
                raise errors.BzrError(
227
 
                               'external diff failed with exit code 2'
228
 
                               ' when run with LANG=C and LC_ALL=C,'
229
 
                               ' but not when run natively: %r' % (diffcmd,))
230
 
 
231
 
            first_line = lang_c_out.split('\n', 1)[0]
232
 
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
233
 
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
234
 
            if m is None:
235
 
                raise errors.BzrError('external diff failed with exit code 2;'
236
 
                                      ' command: %r' % (diffcmd,))
237
 
            else:
238
 
                # Binary files differ, just return
239
 
                return
240
 
 
241
 
        # If we got to here, we haven't written out the output of diff
242
 
        # do so now
243
 
        to_file.write(out)
244
 
        if rc not in (0, 1):
245
 
            # returns 1 if files differ; that's OK
246
 
            if rc < 0:
247
 
                msg = 'signal %d' % (-rc)
248
 
            else:
249
 
                msg = 'exit code %d' % rc
250
 
                
251
 
            raise errors.BzrError('external diff failed with %s; command: %r' 
252
 
                                  % (rc, diffcmd))
253
 
 
254
 
 
255
 
    finally:
256
 
        oldtmpf.close()                 # and delete
257
 
        newtmpf.close()
258
 
        # Clean up. Warn in case the files couldn't be deleted
259
 
        # (in case windows still holds the file open, but not
260
 
        # if the files have already been deleted)
261
 
        try:
262
 
            os.remove(old_abspath)
263
 
        except OSError, e:
264
 
            if e.errno not in (errno.ENOENT,):
265
 
                warning('Failed to delete temporary file: %s %s',
266
 
                        old_abspath, e)
267
 
        try:
268
 
            os.remove(new_abspath)
269
 
        except OSError:
270
 
            if e.errno not in (errno.ENOENT,):
271
 
                warning('Failed to delete temporary file: %s %s',
272
 
                        new_abspath, e)
273
 
 
274
 
 
275
 
@deprecated_function(zero_ninetythree)
276
 
def diff_cmd_helper(tree, specific_files, external_diff_options, 
277
 
                    old_revision_spec=None, new_revision_spec=None,
278
 
                    revision_specs=None,
279
 
                    old_label='a/', new_label='b/'):
280
 
    """Helper for cmd_diff.
281
 
 
282
 
    :param tree:
283
 
        A WorkingTree
284
 
 
285
 
    :param specific_files:
286
 
        The specific files to compare, or None
287
 
 
288
 
    :param external_diff_options:
289
 
        If non-None, run an external diff, and pass it these options
290
 
 
291
 
    :param old_revision_spec:
292
 
        If None, use basis tree as old revision, otherwise use the tree for
293
 
        the specified revision. 
294
 
 
295
 
    :param new_revision_spec:
296
 
        If None, use working tree as new revision, otherwise use the tree for
297
 
        the specified revision.
298
 
    
299
 
    :param revision_specs: 
300
 
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
301
 
        to compare.  This can be passed as an alternative to the old_revision_spec 
302
 
        and new_revision_spec parameters.
303
 
 
304
 
    The more general form is show_diff_trees(), where the caller
305
 
    supplies any two trees.
306
 
    """
307
 
 
308
 
    # TODO: perhaps remove the old parameters old_revision_spec and
309
 
    # new_revision_spec, since this is only really for use from cmd_diff and
310
 
    # it now always passes through a sequence of revision_specs -- mbp
311
 
    # 20061221
312
 
 
313
 
    def spec_tree(spec):
314
 
        if tree:
315
 
            revision = spec.in_store(tree.branch)
316
 
        else:
317
 
            revision = spec.in_store(None)
318
 
        revision_id = revision.rev_id
319
 
        branch = revision.branch
320
 
        return branch.repository.revision_tree(revision_id)
321
 
 
322
 
    if revision_specs is not None:
323
 
        assert (old_revision_spec is None
324
 
                and new_revision_spec is None)
325
 
        if len(revision_specs) > 0:
326
 
            old_revision_spec = revision_specs[0]
327
 
        if len(revision_specs) > 1:
328
 
            new_revision_spec = revision_specs[1]
329
 
 
330
 
    if old_revision_spec is None:
331
 
        old_tree = tree.basis_tree()
332
 
    else:
333
 
        old_tree = spec_tree(old_revision_spec)
334
 
 
335
 
    if (new_revision_spec is None
336
 
        or new_revision_spec.spec is None):
337
 
        new_tree = tree
338
 
    else:
339
 
        new_tree = spec_tree(new_revision_spec)
340
 
 
341
 
    if new_tree is not tree:
342
 
        extra_trees = (tree,)
343
 
    else:
344
 
        extra_trees = None
345
 
 
346
 
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
347
 
                           external_diff_options,
348
 
                           old_label=old_label, new_label=new_label,
349
 
                           extra_trees=extra_trees)
350
 
 
351
 
 
352
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
353
 
    """Get the trees and specific files to diff given a list of paths.
354
 
 
355
 
    This method works out the trees to be diff'ed and the files of
356
 
    interest within those trees.
357
 
 
358
 
    :param path_list:
359
 
        the list of arguments passed to the diff command
360
 
    :param revision_specs:
361
 
        Zero, one or two RevisionSpecs from the diff command line,
362
 
        saying what revisions to compare.
363
 
    :param old_url:
364
 
        The url of the old branch or tree. If None, the tree to use is
365
 
        taken from the first path, if any, or the current working tree.
366
 
    :param new_url:
367
 
        The url of the new branch or tree. If None, the tree to use is
368
 
        taken from the first path, if any, or the current working tree.
369
 
    :returns:
370
 
        a tuple of (old_tree, new_tree, specific_files, extra_trees) where
371
 
        extra_trees is a sequence of additional trees to search in for
372
 
        file-ids.
373
 
    """
374
 
    # Get the old and new revision specs
375
 
    old_revision_spec = None
376
 
    new_revision_spec = None
377
 
    if revision_specs is not None:
378
 
        if len(revision_specs) > 0:
379
 
            old_revision_spec = revision_specs[0]
380
 
            if old_url is None:
381
 
                old_url = old_revision_spec.get_branch()
382
 
        if len(revision_specs) > 1:
383
 
            new_revision_spec = revision_specs[1]
384
 
            if new_url is None:
385
 
                new_url = new_revision_spec.get_branch()
386
 
 
387
 
    other_paths = []
388
 
    make_paths_wt_relative = True
389
 
    if path_list is None or len(path_list) == 0:
390
 
        # If no path is given, assume the current directory
391
 
        default_location = u'.'
392
 
    elif old_url is not None and new_url is not None:
393
 
        other_paths = path_list
394
 
        make_paths_wt_relative = False
395
 
    else:
396
 
        default_location = path_list[0]
397
 
        other_paths = path_list[1:]
398
 
 
399
 
    # Get the old location
400
 
    specific_files = []
401
 
    if old_url is None:
402
 
        old_url = default_location
403
 
    working_tree, branch, relpath = \
404
 
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
405
 
    if relpath != '':
406
 
        specific_files.append(relpath)
407
 
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
408
 
 
409
 
    # Get the new location
410
 
    if new_url is None:
411
 
        new_url = default_location
412
 
    if new_url != old_url:
413
 
        working_tree, branch, relpath = \
414
 
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
415
 
        if relpath != '':
416
 
            specific_files.append(relpath)
417
 
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
418
 
        basis_is_default=working_tree is None)
419
 
 
420
 
    # Get the specific files (all files is None, no files is [])
421
 
    if make_paths_wt_relative and working_tree is not None:
422
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
423
 
    specific_files.extend(other_paths)
424
 
    if len(specific_files) == 0:
425
 
        specific_files = None
426
 
 
427
 
    # Get extra trees that ought to be searched for file-ids
428
 
    extra_trees = None
429
 
    if working_tree is not None and working_tree not in (old_tree, new_tree):
430
 
        extra_trees = (working_tree,)
431
 
    return old_tree, new_tree, specific_files, extra_trees
432
 
 
433
 
 
434
 
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
435
 
    if branch is None and tree is not None:
436
 
        branch = tree.branch
437
 
    if spec is None or spec.spec is None:
438
 
        if basis_is_default:
439
 
            if tree is not None:
440
 
                return tree.basis_tree()
441
 
            else:
442
 
                return branch.basis_tree()
443
 
        else:
444
 
            return tree
445
 
    revision = spec.in_store(branch)
446
 
    revision_id = revision.rev_id
447
 
    rev_branch = revision.branch
448
 
    return rev_branch.repository.revision_tree(revision_id)
449
 
 
450
 
 
451
 
def _relative_paths_in_tree(tree, paths):
452
 
    """Get the relative paths within a working tree.
453
 
 
454
 
    Each path may be either an absolute path or a path relative to the
455
 
    current working directory.
456
 
    """
457
 
    result = []
458
 
    for filename in paths:
459
 
        try:
460
 
            result.append(tree.relpath(osutils.dereference_path(filename)))
461
 
        except errors.PathNotChild:
462
 
            raise errors.BzrCommandError("Files are in different branches")
463
 
    return result
464
 
 
465
 
 
466
 
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
467
 
                    external_diff_options=None,
468
 
                    old_label='a/', new_label='b/',
469
 
                    extra_trees=None,
470
 
                    path_encoding='utf8'):
471
 
    """Show in text form the changes from one tree to another.
472
 
 
473
 
    to_file
474
 
        The output stream.
475
 
 
476
 
    specific_files
477
 
        Include only changes to these files - None for all changes.
478
 
 
479
 
    external_diff_options
480
 
        If set, use an external GNU diff and pass these options.
481
 
 
482
 
    extra_trees
483
 
        If set, more Trees to use for looking up file ids
484
 
 
485
 
    path_encoding
486
 
        If set, the path will be encoded as specified, otherwise is supposed
487
 
        to be utf8
488
 
    """
489
 
    old_tree.lock_read()
490
 
    try:
491
 
        if extra_trees is not None:
492
 
            for tree in extra_trees:
493
 
                tree.lock_read()
494
 
        new_tree.lock_read()
495
 
        try:
496
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
497
 
                                                   path_encoding,
498
 
                                                   external_diff_options,
499
 
                                                   old_label, new_label)
500
 
            return differ.show_diff(specific_files, extra_trees)
501
 
        finally:
502
 
            new_tree.unlock()
503
 
            if extra_trees is not None:
504
 
                for tree in extra_trees:
505
 
                    tree.unlock()
506
 
    finally:
507
 
        old_tree.unlock()
508
 
 
509
 
 
510
 
def _patch_header_date(tree, file_id, path):
511
 
    """Returns a timestamp suitable for use in a patch header."""
512
 
    mtime = tree.get_file_mtime(file_id, path)
513
 
    assert mtime is not None, \
514
 
        "got an mtime of None for file-id %s, path %s in tree %s" % (
515
 
                file_id, path, tree)
516
 
    return timestamp.format_patch_date(mtime)
517
 
 
518
 
 
519
 
def _raise_if_nonexistent(paths, old_tree, new_tree):
520
 
    """Complain if paths are not in either inventory or tree.
521
 
 
522
 
    It's OK with the files exist in either tree's inventory, or 
523
 
    if they exist in the tree but are not versioned.
524
 
    
525
 
    This can be used by operations such as bzr status that can accept
526
 
    unknown or ignored files.
527
 
    """
528
 
    mutter("check paths: %r", paths)
529
 
    if not paths:
530
 
        return
531
 
    s = old_tree.filter_unversioned_files(paths)
532
 
    s = new_tree.filter_unversioned_files(s)
533
 
    s = [path for path in s if not new_tree.has_filename(path)]
534
 
    if s:
535
 
        raise errors.PathsDoNotExist(sorted(s))
536
 
 
537
 
 
538
 
def get_prop_change(meta_modified):
539
 
    if meta_modified:
540
 
        return " (properties changed)"
541
 
    else:
542
 
        return  ""
543
 
 
544
 
 
545
 
class DiffPath(object):
546
 
    """Base type for command object that compare files"""
547
 
 
548
 
    # The type or contents of the file were unsuitable for diffing
549
 
    CANNOT_DIFF = 'CANNOT_DIFF'
550
 
    # The file has changed in a semantic way
551
 
    CHANGED = 'CHANGED'
552
 
    # The file content may have changed, but there is no semantic change
553
 
    UNCHANGED = 'UNCHANGED'
554
 
 
555
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
556
 
        """Constructor.
557
 
 
558
 
        :param old_tree: The tree to show as the old tree in the comparison
559
 
        :param new_tree: The tree to show as new in the comparison
560
 
        :param to_file: The file to write comparison data to
561
 
        :param path_encoding: The character encoding to write paths in
562
 
        """
563
 
        self.old_tree = old_tree
564
 
        self.new_tree = new_tree
565
 
        self.to_file = to_file
566
 
        self.path_encoding = path_encoding
567
 
 
568
 
    @classmethod
569
 
    def from_diff_tree(klass, diff_tree):
570
 
        return klass(diff_tree.old_tree, diff_tree.new_tree,
571
 
                     diff_tree.to_file, diff_tree.path_encoding)
572
 
 
573
 
    @staticmethod
574
 
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
575
 
        for file_differ in differs:
576
 
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
577
 
                                      new_kind)
578
 
            if result is not DiffPath.CANNOT_DIFF:
579
 
                return result
580
 
        else:
581
 
            return DiffPath.CANNOT_DIFF
582
 
 
583
 
 
584
 
class DiffKindChange(object):
585
 
    """Special differ for file kind changes.
586
 
 
587
 
    Represents kind change as deletion + creation.  Uses the other differs
588
 
    to do this.
589
 
    """
590
 
    def __init__(self, differs):
591
 
        self.differs = differs
592
 
 
593
 
    @classmethod
594
 
    def from_diff_tree(klass, diff_tree):
595
 
        return klass(diff_tree.differs)
596
 
 
597
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
598
 
        """Perform comparison
599
 
 
600
 
        :param file_id: The file_id of the file to compare
601
 
        :param old_path: Path of the file in the old tree
602
 
        :param new_path: Path of the file in the new tree
603
 
        :param old_kind: Old file-kind of the file
604
 
        :param new_kind: New file-kind of the file
605
 
        """
606
 
        if None in (old_kind, new_kind):
607
 
            return DiffPath.CANNOT_DIFF
608
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
609
 
                                       new_path, old_kind, None)
610
 
        if result is DiffPath.CANNOT_DIFF:
611
 
            return result
612
 
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
613
 
                                     None, new_kind)
614
 
 
615
 
 
616
 
class DiffDirectory(DiffPath):
617
 
 
618
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
619
 
        """Perform comparison between two directories.  (dummy)
620
 
 
621
 
        """
622
 
        if 'directory' not in (old_kind, new_kind):
623
 
            return self.CANNOT_DIFF
624
 
        if old_kind not in ('directory', None):
625
 
            return self.CANNOT_DIFF
626
 
        if new_kind not in ('directory', None):
627
 
            return self.CANNOT_DIFF
628
 
        return self.CHANGED
629
 
 
630
 
 
631
 
class DiffSymlink(DiffPath):
632
 
 
633
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
634
 
        """Perform comparison between two symlinks
635
 
 
636
 
        :param file_id: The file_id of the file to compare
637
 
        :param old_path: Path of the file in the old tree
638
 
        :param new_path: Path of the file in the new tree
639
 
        :param old_kind: Old file-kind of the file
640
 
        :param new_kind: New file-kind of the file
641
 
        """
642
 
        if 'symlink' not in (old_kind, new_kind):
643
 
            return self.CANNOT_DIFF
644
 
        if old_kind == 'symlink':
645
 
            old_target = self.old_tree.get_symlink_target(file_id)
646
 
        elif old_kind is None:
647
 
            old_target = None
648
 
        else:
649
 
            return self.CANNOT_DIFF
650
 
        if new_kind == 'symlink':
651
 
            new_target = self.new_tree.get_symlink_target(file_id)
652
 
        elif new_kind is None:
653
 
            new_target = None
654
 
        else:
655
 
            return self.CANNOT_DIFF
656
 
        return self.diff_symlink(old_target, new_target)
657
 
 
658
 
    def diff_symlink(self, old_target, new_target):
659
 
        if old_target is None:
660
 
            self.to_file.write('=== target is %r\n' % new_target)
661
 
        elif new_target is None:
662
 
            self.to_file.write('=== target was %r\n' % old_target)
663
 
        else:
664
 
            self.to_file.write('=== target changed %r => %r\n' %
665
 
                              (old_target, new_target))
666
 
        return self.CHANGED
667
 
 
668
 
 
669
 
class DiffText(DiffPath):
670
 
 
671
 
    # GNU Patch uses the epoch date to detect files that are being added
672
 
    # or removed in a diff.
673
 
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
674
 
 
675
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
676
 
                 old_label='', new_label='', text_differ=internal_diff):
677
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
678
 
        self.text_differ = text_differ
679
 
        self.old_label = old_label
680
 
        self.new_label = new_label
681
 
        self.path_encoding = path_encoding
682
 
 
683
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
684
 
        """Compare two files in unified diff format
685
 
 
686
 
        :param file_id: The file_id of the file to compare
687
 
        :param old_path: Path of the file in the old tree
688
 
        :param new_path: Path of the file in the new tree
689
 
        :param old_kind: Old file-kind of the file
690
 
        :param new_kind: New file-kind of the file
691
 
        """
692
 
        if 'file' not in (old_kind, new_kind):
693
 
            return self.CANNOT_DIFF
694
 
        from_file_id = to_file_id = file_id
695
 
        if old_kind == 'file':
696
 
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
697
 
        elif old_kind is None:
698
 
            old_date = self.EPOCH_DATE
699
 
            from_file_id = None
700
 
        else:
701
 
            return self.CANNOT_DIFF
702
 
        if new_kind == 'file':
703
 
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
704
 
        elif new_kind is None:
705
 
            new_date = self.EPOCH_DATE
706
 
            to_file_id = None
707
 
        else:
708
 
            return self.CANNOT_DIFF
709
 
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
710
 
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
711
 
        return self.diff_text(from_file_id, to_file_id, from_label, to_label)
712
 
 
713
 
    def diff_text(self, from_file_id, to_file_id, from_label, to_label):
714
 
        """Diff the content of given files in two trees
715
 
 
716
 
        :param from_file_id: The id of the file in the from tree.  If None,
717
 
            the file is not present in the from tree.
718
 
        :param to_file_id: The id of the file in the to tree.  This may refer
719
 
            to a different file from from_file_id.  If None,
720
 
            the file is not present in the to tree.
721
 
        """
722
 
        def _get_text(tree, file_id):
723
 
            if file_id is not None:
724
 
                return tree.get_file(file_id).readlines()
725
 
            else:
726
 
                return []
727
 
        try:
728
 
            from_text = _get_text(self.old_tree, from_file_id)
729
 
            to_text = _get_text(self.new_tree, to_file_id)
730
 
            self.text_differ(from_label, from_text, to_label, to_text,
731
 
                             self.to_file)
732
 
        except errors.BinaryFile:
733
 
            self.to_file.write(
734
 
                  ("Binary files %s and %s differ\n" %
735
 
                  (from_label, to_label)).encode(self.path_encoding))
736
 
        return self.CHANGED
737
 
 
738
 
 
739
 
class DiffTree(object):
740
 
    """Provides textual representations of the difference between two trees.
741
 
 
742
 
    A DiffTree examines two trees and where a file-id has altered
743
 
    between them, generates a textual representation of the difference.
744
 
    DiffTree uses a sequence of DiffPath objects which are each
745
 
    given the opportunity to handle a given altered fileid. The list
746
 
    of DiffPath objects can be extended globally by appending to
747
 
    DiffTree.diff_factories, or for a specific diff operation by
748
 
    supplying the extra_factories option to the appropriate method.
749
 
    """
750
 
 
751
 
    # list of factories that can provide instances of DiffPath objects
752
 
    # may be extended by plugins.
753
 
    diff_factories = [DiffSymlink.from_diff_tree,
754
 
                      DiffDirectory.from_diff_tree]
755
 
 
756
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
757
 
                 diff_text=None, extra_factories=None):
758
 
        """Constructor
759
 
 
760
 
        :param old_tree: Tree to show as old in the comparison
761
 
        :param new_tree: Tree to show as new in the comparison
762
 
        :param to_file: File to write comparision to
763
 
        :param path_encoding: Character encoding to write paths in
764
 
        :param diff_text: DiffPath-type object to use as a last resort for
765
 
            diffing text files.
766
 
        :param extra_factories: Factories of DiffPaths to try before any other
767
 
            DiffPaths"""
768
 
        if diff_text is None:
769
 
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
770
 
                                 '', '',  internal_diff)
771
 
        self.old_tree = old_tree
772
 
        self.new_tree = new_tree
773
 
        self.to_file = to_file
774
 
        self.path_encoding = path_encoding
775
 
        self.differs = []
776
 
        if extra_factories is not None:
777
 
            self.differs.extend(f(self) for f in extra_factories)
778
 
        self.differs.extend(f(self) for f in self.diff_factories)
779
 
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
780
 
 
781
 
    @classmethod
782
 
    def from_trees_options(klass, old_tree, new_tree, to_file,
783
 
                           path_encoding, external_diff_options, old_label,
784
 
                           new_label):
785
 
        """Factory for producing a DiffTree.
786
 
 
787
 
        Designed to accept options used by show_diff_trees.
788
 
        :param old_tree: The tree to show as old in the comparison
789
 
        :param new_tree: The tree to show as new in the comparison
790
 
        :param to_file: File to write comparisons to
791
 
        :param path_encoding: Character encoding to use for writing paths
792
 
        :param external_diff_options: If supplied, use the installed diff
793
 
            binary to perform file comparison, using supplied options.
794
 
        :param old_label: Prefix to use for old file labels
795
 
        :param new_label: Prefix to use for new file labels
796
 
        """
797
 
        if external_diff_options:
798
 
            assert isinstance(external_diff_options, basestring)
799
 
            opts = external_diff_options.split()
800
 
            def diff_file(olab, olines, nlab, nlines, to_file):
801
 
                external_diff(olab, olines, nlab, nlines, to_file, opts)
802
 
        else:
803
 
            diff_file = internal_diff
804
 
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
805
 
                             old_label, new_label, diff_file)
806
 
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text)
807
 
 
808
 
    def show_diff(self, specific_files, extra_trees=None):
809
 
        """Write tree diff to self.to_file
810
 
 
811
 
        :param sepecific_files: the specific files to compare (recursive)
812
 
        :param extra_trees: extra trees to use for mapping paths to file_ids
813
 
        """
814
 
        # TODO: Generation of pseudo-diffs for added/deleted files could
815
 
        # be usefully made into a much faster special case.
816
 
 
817
 
        delta = self.new_tree.changes_from(self.old_tree,
818
 
            specific_files=specific_files,
819
 
            extra_trees=extra_trees, require_versioned=True)
820
 
 
821
 
        has_changes = 0
822
 
        for path, file_id, kind in delta.removed:
823
 
            has_changes = 1
824
 
            path_encoded = path.encode(self.path_encoding, "replace")
825
 
            self.to_file.write("=== removed %s '%s'\n" % (kind, path_encoded))
826
 
            self.diff(file_id, path, path)
827
 
 
828
 
        for path, file_id, kind in delta.added:
829
 
            has_changes = 1
830
 
            path_encoded = path.encode(self.path_encoding, "replace")
831
 
            self.to_file.write("=== added %s '%s'\n" % (kind, path_encoded))
832
 
            self.diff(file_id, path, path)
833
 
        for (old_path, new_path, file_id, kind,
834
 
             text_modified, meta_modified) in delta.renamed:
835
 
            has_changes = 1
836
 
            prop_str = get_prop_change(meta_modified)
837
 
            oldpath_encoded = old_path.encode(self.path_encoding, "replace")
838
 
            newpath_encoded = new_path.encode(self.path_encoding, "replace")
839
 
            self.to_file.write("=== renamed %s '%s' => '%s'%s\n" % (kind,
840
 
                                oldpath_encoded, newpath_encoded, prop_str))
841
 
            if text_modified:
842
 
                self.diff(file_id, old_path, new_path)
843
 
        for path, file_id, kind, text_modified, meta_modified in\
844
 
            delta.modified:
845
 
            has_changes = 1
846
 
            prop_str = get_prop_change(meta_modified)
847
 
            path_encoded = path.encode(self.path_encoding, "replace")
848
 
            self.to_file.write("=== modified %s '%s'%s\n" % (kind,
849
 
                                path_encoded, prop_str))
850
 
            # The file may be in a different location in the old tree (because
851
 
            # the containing dir was renamed, but the file itself was not)
852
 
            if text_modified:
853
 
                old_path = self.old_tree.id2path(file_id)
854
 
                self.diff(file_id, old_path, path)
855
 
        return has_changes
856
 
 
857
 
    def diff(self, file_id, old_path, new_path):
858
 
        """Perform a diff of a single file
859
 
 
860
 
        :param file_id: file-id of the file
861
 
        :param old_path: The path of the file in the old tree
862
 
        :param new_path: The path of the file in the new tree
863
 
        """
864
 
        try:
865
 
            old_kind = self.old_tree.kind(file_id)
866
 
        except (errors.NoSuchId, errors.NoSuchFile):
867
 
            old_kind = None
868
 
        try:
869
 
            new_kind = self.new_tree.kind(file_id)
870
 
        except (errors.NoSuchId, errors.NoSuchFile):
871
 
            new_kind = None
872
 
 
873
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
874
 
                                       new_path, old_kind, new_kind)
875
 
        if result is DiffPath.CANNOT_DIFF:
876
 
            error_path = new_path
877
 
            if error_path is None:
878
 
                error_path = old_path
879
 
            raise errors.NoDiffFound(error_path)