/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: Jelmer Vernooij
  • Date: 2009-04-02 15:54:49 UTC
  • mto: (0.200.326 trunk)
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@samba.org-20090402155449-nuqhu1fsnqk6bt0g
Check that regenerated objects have the expected sha1.

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