~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Adeodato Simó
  • Date: 2006-07-28 16:05:23 UTC
  • mto: (1558.14.10 Aaron's integration)
  • mto: This revision was merged to the branch mainline in revision 1889.
  • Revision ID: dato@net.com.org.es-20060728160523-8be0e372b34f3c89
Do not separate paragraphs in the copyright statement with blank lines,
but with lines containing only #.

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
import difflib
 
17
import errno
18
18
import os
19
 
import re
 
19
import subprocess
20
20
import sys
21
 
 
22
 
from bzrlib.lazy_import import lazy_import
23
 
lazy_import(globals(), """
24
 
import errno
25
 
import subprocess
26
21
import tempfile
27
22
import time
28
23
 
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
 
        )
 
24
from bzrlib.delta import compare_trees
 
25
from bzrlib.errors import BzrError
 
26
import bzrlib.errors as errors
 
27
import bzrlib.osutils
 
28
from bzrlib.patiencediff import unified_diff
 
29
import bzrlib.patiencediff
 
30
from bzrlib.symbol_versioning import (deprecated_function,
 
31
        zero_eight)
 
32
from bzrlib.textfile import check_text_lines
41
33
from bzrlib.trace import mutter, warning
42
34
 
43
35
 
45
37
# invoke callbacks on an object.  That object can either accumulate a
46
38
# list, write them out directly, etc etc.
47
39
 
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
40
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
59
41
                  allow_binary=False, sequence_matcher=None,
60
42
                  path_encoding='utf8'):
75
57
        return
76
58
    
77
59
    if allow_binary is False:
78
 
        textfile.check_text_lines(oldlines)
79
 
        textfile.check_text_lines(newlines)
 
60
        check_text_lines(oldlines)
 
61
        check_text_lines(newlines)
80
62
 
81
63
    if sequence_matcher is None:
82
 
        sequence_matcher = patiencediff.PatienceSequenceMatcher
83
 
    ud = patiencediff.unified_diff(oldlines, newlines,
 
64
        sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
 
65
    ud = unified_diff(oldlines, newlines,
84
66
                      fromfile=old_filename.encode(path_encoding),
85
67
                      tofile=new_filename.encode(path_encoding),
86
68
                      sequencematcher=sequence_matcher)
87
69
 
88
70
    ud = list(ud)
89
 
    if len(ud) == 0: # Identical contents, nothing to do
90
 
        return
91
71
    # work-around for difflib being too smart for its own good
92
72
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
93
73
    if not oldlines:
102
82
        to_file.write(line)
103
83
        if not line.endswith('\n'):
104
84
            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
 
85
    print >>to_file
143
86
 
144
87
 
145
88
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
146
89
                  diff_opts):
147
90
    """Display a diff by calling out to the external diff program."""
 
91
    if hasattr(to_file, 'fileno'):
 
92
        out_file = to_file
 
93
        have_fileno = True
 
94
    else:
 
95
        out_file = subprocess.PIPE
 
96
        have_fileno = False
 
97
    
148
98
    # make sure our own output is properly ordered before the diff
149
99
    to_file.flush()
150
100
 
201
151
        if diff_opts:
202
152
            diffcmd.extend(diff_opts)
203
153
 
204
 
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
205
 
        out,err = pipe.communicate()
206
 
        rc = pipe.returncode
 
154
        try:
 
155
            pipe = subprocess.Popen(diffcmd,
 
156
                                    stdin=subprocess.PIPE,
 
157
                                    stdout=out_file)
 
158
        except OSError, e:
 
159
            if e.errno == errno.ENOENT:
 
160
                raise errors.NoDiff(str(e))
 
161
            raise
 
162
        pipe.stdin.close()
 
163
 
 
164
        if not have_fileno:
 
165
            bzrlib.osutils.pumpfile(pipe.stdout, to_file)
 
166
        rc = pipe.wait()
207
167
        
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):
 
168
        if rc != 0 and rc != 1:
243
169
            # returns 1 if files differ; that's OK
244
170
            if rc < 0:
245
171
                msg = 'signal %d' % (-rc)
246
172
            else:
247
173
                msg = 'exit code %d' % rc
248
174
                
249
 
            raise errors.BzrError('external diff failed with %s; command: %r' 
250
 
                                  % (rc, diffcmd))
251
 
 
252
 
 
 
175
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
253
176
    finally:
254
177
        oldtmpf.close()                 # and delete
255
178
        newtmpf.close()
270
193
                        new_abspath, e)
271
194
 
272
195
 
 
196
@deprecated_function(zero_eight)
 
197
def show_diff(b, from_spec, specific_files, external_diff_options=None,
 
198
              revision2=None, output=None, b2=None):
 
199
    """Shortcut for showing the diff to the working tree.
 
200
 
 
201
    Please use show_diff_trees instead.
 
202
 
 
203
    b
 
204
        Branch.
 
205
 
 
206
    revision
 
207
        None for 'basis tree', or otherwise the old revision to compare against.
 
208
    
 
209
    The more general form is show_diff_trees(), where the caller
 
210
    supplies any two trees.
 
211
    """
 
212
    if output is None:
 
213
        output = sys.stdout
 
214
 
 
215
    if from_spec is None:
 
216
        old_tree = b.bzrdir.open_workingtree()
 
217
        if b2 is None:
 
218
            old_tree = old_tree = old_tree.basis_tree()
 
219
    else:
 
220
        old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
 
221
 
 
222
    if revision2 is None:
 
223
        if b2 is None:
 
224
            new_tree = b.bzrdir.open_workingtree()
 
225
        else:
 
226
            new_tree = b2.bzrdir.open_workingtree()
 
227
    else:
 
228
        new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
 
229
 
 
230
    return show_diff_trees(old_tree, new_tree, output, specific_files,
 
231
                           external_diff_options)
 
232
 
 
233
 
273
234
def diff_cmd_helper(tree, specific_files, external_diff_options, 
274
235
                    old_revision_spec=None, new_revision_spec=None,
275
 
                    revision_specs=None,
276
236
                    old_label='a/', new_label='b/'):
277
237
    """Helper for cmd_diff.
278
238
 
279
 
    :param tree:
 
239
   tree 
280
240
        A WorkingTree
281
241
 
282
 
    :param specific_files:
 
242
    specific_files
283
243
        The specific files to compare, or None
284
244
 
285
 
    :param external_diff_options:
 
245
    external_diff_options
286
246
        If non-None, run an external diff, and pass it these options
287
247
 
288
 
    :param old_revision_spec:
 
248
    old_revision_spec
289
249
        If None, use basis tree as old revision, otherwise use the tree for
290
250
        the specified revision. 
291
251
 
292
 
    :param new_revision_spec:
 
252
    new_revision_spec
293
253
        If None, use working tree as new revision, otherwise use the tree for
294
254
        the specified revision.
295
255
    
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
256
    The more general form is show_diff_trees(), where the caller
302
257
    supplies any two trees.
303
258
    """
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
259
    def spec_tree(spec):
311
260
        if tree:
312
261
            revision = spec.in_store(tree.branch)
315
264
        revision_id = revision.rev_id
316
265
        branch = revision.branch
317
266
        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
267
    if old_revision_spec is None:
328
268
        old_tree = tree.basis_tree()
329
269
    else:
330
270
        old_tree = spec_tree(old_revision_spec)
331
271
 
332
 
    if (new_revision_spec is None
333
 
        or new_revision_spec.spec is None):
 
272
    if new_revision_spec is None:
334
273
        new_tree = tree
335
274
    else:
336
275
        new_tree = spec_tree(new_revision_spec)
337
 
 
338
276
    if new_tree is not tree:
339
277
        extra_trees = (tree,)
340
278
    else:
349
287
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
350
288
                    external_diff_options=None,
351
289
                    old_label='a/', new_label='b/',
352
 
                    extra_trees=None,
353
 
                    path_encoding='utf8'):
 
290
                    extra_trees=None):
354
291
    """Show in text form the changes from one tree to another.
355
292
 
356
293
    to_files
361
298
 
362
299
    extra_trees
363
300
        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
301
    """
369
302
    old_tree.lock_read()
370
303
    try:
371
 
        if extra_trees is not None:
372
 
            for tree in extra_trees:
373
 
                tree.lock_read()
374
304
        new_tree.lock_read()
375
305
        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)
 
306
            return _show_diff_trees(old_tree, new_tree, to_file,
 
307
                                    specific_files, external_diff_options,
 
308
                                    old_label=old_label, new_label=new_label,
 
309
                                    extra_trees=extra_trees)
381
310
        finally:
382
311
            new_tree.unlock()
383
 
            if extra_trees is not None:
384
 
                for tree in extra_trees:
385
 
                    tree.unlock()
386
312
    finally:
387
313
        old_tree.unlock()
388
314
 
389
315
 
 
316
def _show_diff_trees(old_tree, new_tree, to_file,
 
317
                     specific_files, external_diff_options, 
 
318
                     old_label='a/', new_label='b/', extra_trees=None):
 
319
 
 
320
    # GNU Patch uses the epoch date to detect files that are being added
 
321
    # or removed in a diff.
 
322
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
 
323
 
 
324
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
325
    # be usefully made into a much faster special case.
 
326
 
 
327
    if external_diff_options:
 
328
        assert isinstance(external_diff_options, basestring)
 
329
        opts = external_diff_options.split()
 
330
        def diff_file(olab, olines, nlab, nlines, to_file):
 
331
            external_diff(olab, olines, nlab, nlines, to_file, opts)
 
332
    else:
 
333
        diff_file = internal_diff
 
334
    
 
335
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
 
336
                          specific_files=specific_files, 
 
337
                          extra_trees=extra_trees, require_versioned=True)
 
338
 
 
339
    has_changes = 0
 
340
    for path, file_id, kind in delta.removed:
 
341
        has_changes = 1
 
342
        print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
 
343
        old_name = '%s%s\t%s' % (old_label, path,
 
344
                                 _patch_header_date(old_tree, file_id, path))
 
345
        new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
 
346
        old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
 
347
                                         new_name, None, None, to_file)
 
348
    for path, file_id, kind in delta.added:
 
349
        has_changes = 1
 
350
        print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
 
351
        old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
 
352
        new_name = '%s%s\t%s' % (new_label, path,
 
353
                                 _patch_header_date(new_tree, file_id, path))
 
354
        new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
 
355
                                         old_name, None, None, to_file, 
 
356
                                         reverse=True)
 
357
    for (old_path, new_path, file_id, kind,
 
358
         text_modified, meta_modified) in delta.renamed:
 
359
        has_changes = 1
 
360
        prop_str = get_prop_change(meta_modified)
 
361
        print >>to_file, '=== renamed %s %r => %r%s' % (
 
362
                    kind, old_path.encode('utf8'),
 
363
                    new_path.encode('utf8'), prop_str)
 
364
        old_name = '%s%s\t%s' % (old_label, old_path,
 
365
                                 _patch_header_date(old_tree, file_id,
 
366
                                                    old_path))
 
367
        new_name = '%s%s\t%s' % (new_label, new_path,
 
368
                                 _patch_header_date(new_tree, file_id,
 
369
                                                    new_path))
 
370
        _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
371
                                    new_name, new_tree,
 
372
                                    text_modified, kind, to_file, diff_file)
 
373
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
 
374
        has_changes = 1
 
375
        prop_str = get_prop_change(meta_modified)
 
376
        print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
 
377
        old_name = '%s%s\t%s' % (old_label, path,
 
378
                                 _patch_header_date(old_tree, file_id, path))
 
379
        new_name = '%s%s\t%s' % (new_label, path,
 
380
                                 _patch_header_date(new_tree, file_id, path))
 
381
        if text_modified:
 
382
            _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
383
                                        new_name, new_tree,
 
384
                                        True, kind, to_file, diff_file)
 
385
 
 
386
    return has_changes
 
387
 
 
388
 
390
389
def _patch_header_date(tree, file_id, path):
391
390
    """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)
 
391
    tm = time.gmtime(tree.get_file_mtime(file_id, path))
 
392
    return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
397
393
 
398
394
 
399
395
def _raise_if_nonexistent(paths, old_tree, new_tree):
422
418
        return  ""
423
419
 
424
420
 
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)
 
421
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
 
422
                                new_path, new_tree, text_modified,
 
423
                                kind, to_file, diff_file):
 
424
    if text_modified:
 
425
        new_entry = new_tree.inventory[file_id]
 
426
        old_tree.inventory[file_id].diff(diff_file,
 
427
                                         old_path, old_tree,
 
428
                                         new_path, new_entry, 
 
429
                                         new_tree, to_file)