~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Andrew Bennetts
  • Date: 2007-03-26 06:24:01 UTC
  • mto: This revision was merged to the branch mainline in revision 2376.
  • Revision ID: andrew.bennetts@canonical.com-20070326062401-k3nbefzje5332jaf
Deal with review comments from Robert:

  * Add my name to the NEWS file
  * Move the test case to a new module in branch_implementations
  * Remove revision_history cruft from identitymap and test_identitymap
  * Improve some docstrings

Also, this fixes a bug where revision_history was not returning a copy of the
cached data, allowing the cache to be corrupted.

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
18
17
import os
19
18
import re
20
 
import shutil
21
19
import sys
22
20
 
23
21
from bzrlib.lazy_import import lazy_import
28
26
import time
29
27
 
30
28
from bzrlib import (
31
 
    branch as _mod_branch,
32
 
    bzrdir,
33
 
    commands,
34
29
    errors,
35
30
    osutils,
36
31
    patiencediff,
39
34
    )
40
35
""")
41
36
 
 
37
# compatability - plugins import compare_trees from diff!!!
 
38
# deprecated as of 0.10
 
39
from bzrlib.delta import compare_trees
42
40
from bzrlib.symbol_versioning import (
43
41
        deprecated_function,
44
 
        one_three
 
42
        zero_eight,
45
43
        )
46
44
from bzrlib.trace import mutter, warning
47
45
 
50
48
# invoke callbacks on an object.  That object can either accumulate a
51
49
# list, write them out directly, etc etc.
52
50
 
53
 
 
54
 
class _PrematchedMatcher(difflib.SequenceMatcher):
55
 
    """Allow SequenceMatcher operations to use predetermined blocks"""
56
 
 
57
 
    def __init__(self, matching_blocks):
58
 
        difflib.SequenceMatcher(self, None, None)
59
 
        self.matching_blocks = matching_blocks
60
 
        self.opcodes = None
61
 
 
62
 
 
63
51
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
64
52
                  allow_binary=False, sequence_matcher=None,
65
53
                  path_encoding='utf8'):
91
79
                      sequencematcher=sequence_matcher)
92
80
 
93
81
    ud = list(ud)
94
 
    if len(ud) == 0: # Identical contents, nothing to do
95
 
        return
96
82
    # work-around for difflib being too smart for its own good
97
83
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
98
84
    if not oldlines:
107
93
        to_file.write(line)
108
94
        if not line.endswith('\n'):
109
95
            to_file.write("\n\\ No newline at end of file\n")
110
 
    to_file.write('\n')
 
96
    print >>to_file
111
97
 
112
98
 
113
99
def _spawn_external_diff(diffcmd, capture_errors=True):
275
261
                        new_abspath, e)
276
262
 
277
263
 
278
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
279
 
    """Get the trees and specific files to diff given a list of paths.
280
 
 
281
 
    This method works out the trees to be diff'ed and the files of
282
 
    interest within those trees.
283
 
 
284
 
    :param path_list:
285
 
        the list of arguments passed to the diff command
286
 
    :param revision_specs:
287
 
        Zero, one or two RevisionSpecs from the diff command line,
288
 
        saying what revisions to compare.
289
 
    :param old_url:
290
 
        The url of the old branch or tree. If None, the tree to use is
291
 
        taken from the first path, if any, or the current working tree.
292
 
    :param new_url:
293
 
        The url of the new branch or tree. If None, the tree to use is
294
 
        taken from the first path, if any, or the current working tree.
295
 
    :returns:
296
 
        a tuple of (old_tree, new_tree, specific_files, extra_trees) where
297
 
        extra_trees is a sequence of additional trees to search in for
298
 
        file-ids.
299
 
    """
300
 
    # Get the old and new revision specs
301
 
    old_revision_spec = None
302
 
    new_revision_spec = None
 
264
@deprecated_function(zero_eight)
 
265
def show_diff(b, from_spec, specific_files, external_diff_options=None,
 
266
              revision2=None, output=None, b2=None):
 
267
    """Shortcut for showing the diff to the working tree.
 
268
 
 
269
    Please use show_diff_trees instead.
 
270
 
 
271
    b
 
272
        Branch.
 
273
 
 
274
    revision
 
275
        None for 'basis tree', or otherwise the old revision to compare against.
 
276
    
 
277
    The more general form is show_diff_trees(), where the caller
 
278
    supplies any two trees.
 
279
    """
 
280
    if output is None:
 
281
        output = sys.stdout
 
282
 
 
283
    if from_spec is None:
 
284
        old_tree = b.bzrdir.open_workingtree()
 
285
        if b2 is None:
 
286
            old_tree = old_tree = old_tree.basis_tree()
 
287
    else:
 
288
        old_tree = b.repository.revision_tree(from_spec.in_history(b).rev_id)
 
289
 
 
290
    if revision2 is None:
 
291
        if b2 is None:
 
292
            new_tree = b.bzrdir.open_workingtree()
 
293
        else:
 
294
            new_tree = b2.bzrdir.open_workingtree()
 
295
    else:
 
296
        new_tree = b.repository.revision_tree(revision2.in_history(b).rev_id)
 
297
 
 
298
    return show_diff_trees(old_tree, new_tree, output, specific_files,
 
299
                           external_diff_options)
 
300
 
 
301
 
 
302
def diff_cmd_helper(tree, specific_files, external_diff_options, 
 
303
                    old_revision_spec=None, new_revision_spec=None,
 
304
                    revision_specs=None,
 
305
                    old_label='a/', new_label='b/'):
 
306
    """Helper for cmd_diff.
 
307
 
 
308
    :param tree:
 
309
        A WorkingTree
 
310
 
 
311
    :param specific_files:
 
312
        The specific files to compare, or None
 
313
 
 
314
    :param external_diff_options:
 
315
        If non-None, run an external diff, and pass it these options
 
316
 
 
317
    :param old_revision_spec:
 
318
        If None, use basis tree as old revision, otherwise use the tree for
 
319
        the specified revision. 
 
320
 
 
321
    :param new_revision_spec:
 
322
        If None, use working tree as new revision, otherwise use the tree for
 
323
        the specified revision.
 
324
    
 
325
    :param revision_specs: 
 
326
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
 
327
        to compare.  This can be passed as an alternative to the old_revision_spec 
 
328
        and new_revision_spec parameters.
 
329
 
 
330
    The more general form is show_diff_trees(), where the caller
 
331
    supplies any two trees.
 
332
    """
 
333
 
 
334
    # TODO: perhaps remove the old parameters old_revision_spec and
 
335
    # new_revision_spec, since this is only really for use from cmd_diff and
 
336
    # it now always passes through a sequence of revision_specs -- mbp
 
337
    # 20061221
 
338
 
 
339
    def spec_tree(spec):
 
340
        if tree:
 
341
            revision = spec.in_store(tree.branch)
 
342
        else:
 
343
            revision = spec.in_store(None)
 
344
        revision_id = revision.rev_id
 
345
        branch = revision.branch
 
346
        return branch.repository.revision_tree(revision_id)
 
347
 
303
348
    if revision_specs is not None:
 
349
        assert (old_revision_spec is None
 
350
                and new_revision_spec is None)
304
351
        if len(revision_specs) > 0:
305
352
            old_revision_spec = revision_specs[0]
306
 
            if old_url is None:
307
 
                old_url = old_revision_spec.get_branch()
308
353
        if len(revision_specs) > 1:
309
354
            new_revision_spec = revision_specs[1]
310
 
            if new_url is None:
311
 
                new_url = new_revision_spec.get_branch()
312
 
 
313
 
    other_paths = []
314
 
    make_paths_wt_relative = True
315
 
    consider_relpath = True
316
 
    if path_list is None or len(path_list) == 0:
317
 
        # If no path is given, the current working tree is used
318
 
        default_location = u'.'
319
 
        consider_relpath = False
320
 
    elif old_url is not None and new_url is not None:
321
 
        other_paths = path_list
322
 
        make_paths_wt_relative = False
323
 
    else:
324
 
        default_location = path_list[0]
325
 
        other_paths = path_list[1:]
326
 
 
327
 
    # Get the old location
328
 
    specific_files = []
329
 
    if old_url is None:
330
 
        old_url = default_location
331
 
    working_tree, branch, relpath = \
332
 
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
333
 
    if consider_relpath and relpath != '':
334
 
        specific_files.append(relpath)
335
 
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
336
 
 
337
 
    # Get the new location
338
 
    if new_url is None:
339
 
        new_url = default_location
340
 
    if new_url != old_url:
341
 
        working_tree, branch, relpath = \
342
 
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
343
 
        if consider_relpath and relpath != '':
344
 
            specific_files.append(relpath)
345
 
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
346
 
        basis_is_default=working_tree is None)
347
 
 
348
 
    # Get the specific files (all files is None, no files is [])
349
 
    if make_paths_wt_relative and working_tree is not None:
350
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
351
 
    specific_files.extend(other_paths)
352
 
    if len(specific_files) == 0:
353
 
        specific_files = None
354
 
 
355
 
    # Get extra trees that ought to be searched for file-ids
356
 
    extra_trees = None
357
 
    if working_tree is not None and working_tree not in (old_tree, new_tree):
358
 
        extra_trees = (working_tree,)
359
 
    return old_tree, new_tree, specific_files, extra_trees
360
 
 
361
 
 
362
 
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
363
 
    if branch is None and tree is not None:
364
 
        branch = tree.branch
365
 
    if spec is None or spec.spec is None:
366
 
        if basis_is_default:
367
 
            if tree is not None:
368
 
                return tree.basis_tree()
369
 
            else:
370
 
                return branch.basis_tree()
371
 
        else:
372
 
            return tree
373
 
    if not spec.needs_branch():
374
 
        branch = _mod_branch.Branch.open(spec.get_branch())
375
 
    revision_id = spec.as_revision_id(branch)
376
 
    return branch.repository.revision_tree(revision_id)
377
 
 
378
 
 
379
 
def _relative_paths_in_tree(tree, paths):
380
 
    """Get the relative paths within a working tree.
381
 
 
382
 
    Each path may be either an absolute path or a path relative to the
383
 
    current working directory.
384
 
    """
385
 
    result = []
386
 
    for filename in paths:
387
 
        try:
388
 
            result.append(tree.relpath(osutils.dereference_path(filename)))
389
 
        except errors.PathNotChild:
390
 
            raise errors.BzrCommandError("Files are in different branches")
391
 
    return result
 
355
 
 
356
    if old_revision_spec is None:
 
357
        old_tree = tree.basis_tree()
 
358
    else:
 
359
        old_tree = spec_tree(old_revision_spec)
 
360
 
 
361
    if (new_revision_spec is None
 
362
        or new_revision_spec.spec is None):
 
363
        new_tree = tree
 
364
    else:
 
365
        new_tree = spec_tree(new_revision_spec)
 
366
 
 
367
    if new_tree is not tree:
 
368
        extra_trees = (tree,)
 
369
    else:
 
370
        extra_trees = None
 
371
 
 
372
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
 
373
                           external_diff_options,
 
374
                           old_label=old_label, new_label=new_label,
 
375
                           extra_trees=extra_trees)
392
376
 
393
377
 
394
378
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
395
379
                    external_diff_options=None,
396
380
                    old_label='a/', new_label='b/',
397
 
                    extra_trees=None,
398
 
                    path_encoding='utf8',
399
 
                    using=None):
 
381
                    extra_trees=None):
400
382
    """Show in text form the changes from one tree to another.
401
383
 
402
 
    to_file
403
 
        The output stream.
404
 
 
405
 
    specific_files
406
 
        Include only changes to these files - None for all changes.
 
384
    to_files
 
385
        If set, include only changes to these files.
407
386
 
408
387
    external_diff_options
409
388
        If set, use an external GNU diff and pass these options.
410
389
 
411
390
    extra_trees
412
391
        If set, more Trees to use for looking up file ids
413
 
 
414
 
    path_encoding
415
 
        If set, the path will be encoded as specified, otherwise is supposed
416
 
        to be utf8
417
392
    """
418
393
    old_tree.lock_read()
419
394
    try:
422
397
                tree.lock_read()
423
398
        new_tree.lock_read()
424
399
        try:
425
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
426
 
                                                 path_encoding,
427
 
                                                 external_diff_options,
428
 
                                                 old_label, new_label, using)
429
 
            return differ.show_diff(specific_files, extra_trees)
 
400
            return _show_diff_trees(old_tree, new_tree, to_file,
 
401
                                    specific_files, external_diff_options,
 
402
                                    old_label=old_label, new_label=new_label,
 
403
                                    extra_trees=extra_trees)
430
404
        finally:
431
405
            new_tree.unlock()
432
406
            if extra_trees is not None:
436
410
        old_tree.unlock()
437
411
 
438
412
 
 
413
def _show_diff_trees(old_tree, new_tree, to_file,
 
414
                     specific_files, external_diff_options, 
 
415
                     old_label='a/', new_label='b/', extra_trees=None):
 
416
 
 
417
    # GNU Patch uses the epoch date to detect files that are being added
 
418
    # or removed in a diff.
 
419
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
 
420
 
 
421
    # TODO: Generation of pseudo-diffs for added/deleted files could
 
422
    # be usefully made into a much faster special case.
 
423
 
 
424
    if external_diff_options:
 
425
        assert isinstance(external_diff_options, basestring)
 
426
        opts = external_diff_options.split()
 
427
        def diff_file(olab, olines, nlab, nlines, to_file):
 
428
            external_diff(olab, olines, nlab, nlines, to_file, opts)
 
429
    else:
 
430
        diff_file = internal_diff
 
431
    
 
432
    delta = new_tree.changes_from(old_tree,
 
433
        specific_files=specific_files,
 
434
        extra_trees=extra_trees, require_versioned=True)
 
435
 
 
436
    has_changes = 0
 
437
    for path, file_id, kind in delta.removed:
 
438
        has_changes = 1
 
439
        print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
 
440
        old_name = '%s%s\t%s' % (old_label, path,
 
441
                                 _patch_header_date(old_tree, file_id, path))
 
442
        new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
 
443
        old_tree.inventory[file_id].diff(diff_file, old_name, old_tree,
 
444
                                         new_name, None, None, to_file)
 
445
    for path, file_id, kind in delta.added:
 
446
        has_changes = 1
 
447
        print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
 
448
        old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
 
449
        new_name = '%s%s\t%s' % (new_label, path,
 
450
                                 _patch_header_date(new_tree, file_id, path))
 
451
        new_tree.inventory[file_id].diff(diff_file, new_name, new_tree,
 
452
                                         old_name, None, None, to_file, 
 
453
                                         reverse=True)
 
454
    for (old_path, new_path, file_id, kind,
 
455
         text_modified, meta_modified) in delta.renamed:
 
456
        has_changes = 1
 
457
        prop_str = get_prop_change(meta_modified)
 
458
        print >>to_file, '=== renamed %s %r => %r%s' % (
 
459
                    kind, old_path.encode('utf8'),
 
460
                    new_path.encode('utf8'), prop_str)
 
461
        old_name = '%s%s\t%s' % (old_label, old_path,
 
462
                                 _patch_header_date(old_tree, file_id,
 
463
                                                    old_path))
 
464
        new_name = '%s%s\t%s' % (new_label, new_path,
 
465
                                 _patch_header_date(new_tree, file_id,
 
466
                                                    new_path))
 
467
        _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
468
                                    new_name, new_tree,
 
469
                                    text_modified, kind, to_file, diff_file)
 
470
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
 
471
        has_changes = 1
 
472
        prop_str = get_prop_change(meta_modified)
 
473
        print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
 
474
        old_name = '%s%s\t%s' % (old_label, path,
 
475
                                 _patch_header_date(old_tree, file_id, path))
 
476
        new_name = '%s%s\t%s' % (new_label, path,
 
477
                                 _patch_header_date(new_tree, file_id, path))
 
478
        if text_modified:
 
479
            _maybe_diff_file_or_symlink(old_name, old_tree, file_id,
 
480
                                        new_name, new_tree,
 
481
                                        True, kind, to_file, diff_file)
 
482
 
 
483
    return has_changes
 
484
 
 
485
 
439
486
def _patch_header_date(tree, file_id, path):
440
487
    """Returns a timestamp suitable for use in a patch header."""
441
 
    mtime = tree.get_file_mtime(file_id, path)
442
 
    return timestamp.format_patch_date(mtime)
 
488
    return timestamp.format_patch_date(tree.get_file_mtime(file_id, path))
443
489
 
444
490
 
445
491
def _raise_if_nonexistent(paths, old_tree, new_tree):
461
507
        raise errors.PathsDoNotExist(sorted(s))
462
508
 
463
509
 
464
 
@deprecated_function(one_three)
465
510
def get_prop_change(meta_modified):
466
511
    if meta_modified:
467
512
        return " (properties changed)"
468
513
    else:
469
514
        return  ""
470
515
 
471
 
def get_executable_change(old_is_x, new_is_x):
472
 
    descr = { True:"+x", False:"-x", None:"??" }
473
 
    if old_is_x != new_is_x:
474
 
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
475
 
    else:
476
 
        return []
477
 
 
478
 
 
479
 
class DiffPath(object):
480
 
    """Base type for command object that compare files"""
481
 
 
482
 
    # The type or contents of the file were unsuitable for diffing
483
 
    CANNOT_DIFF = 'CANNOT_DIFF'
484
 
    # The file has changed in a semantic way
485
 
    CHANGED = 'CHANGED'
486
 
    # The file content may have changed, but there is no semantic change
487
 
    UNCHANGED = 'UNCHANGED'
488
 
 
489
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
490
 
        """Constructor.
491
 
 
492
 
        :param old_tree: The tree to show as the old tree in the comparison
493
 
        :param new_tree: The tree to show as new in the comparison
494
 
        :param to_file: The file to write comparison data to
495
 
        :param path_encoding: The character encoding to write paths in
496
 
        """
497
 
        self.old_tree = old_tree
498
 
        self.new_tree = new_tree
499
 
        self.to_file = to_file
500
 
        self.path_encoding = path_encoding
501
 
 
502
 
    def finish(self):
503
 
        pass
504
 
 
505
 
    @classmethod
506
 
    def from_diff_tree(klass, diff_tree):
507
 
        return klass(diff_tree.old_tree, diff_tree.new_tree,
508
 
                     diff_tree.to_file, diff_tree.path_encoding)
509
 
 
510
 
    @staticmethod
511
 
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
512
 
        for file_differ in differs:
513
 
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
514
 
                                      new_kind)
515
 
            if result is not DiffPath.CANNOT_DIFF:
516
 
                return result
517
 
        else:
518
 
            return DiffPath.CANNOT_DIFF
519
 
 
520
 
 
521
 
class DiffKindChange(object):
522
 
    """Special differ for file kind changes.
523
 
 
524
 
    Represents kind change as deletion + creation.  Uses the other differs
525
 
    to do this.
526
 
    """
527
 
    def __init__(self, differs):
528
 
        self.differs = differs
529
 
 
530
 
    def finish(self):
531
 
        pass
532
 
 
533
 
    @classmethod
534
 
    def from_diff_tree(klass, diff_tree):
535
 
        return klass(diff_tree.differs)
536
 
 
537
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
538
 
        """Perform comparison
539
 
 
540
 
        :param file_id: The file_id of the file to compare
541
 
        :param old_path: Path of the file in the old tree
542
 
        :param new_path: Path of the file in the new tree
543
 
        :param old_kind: Old file-kind of the file
544
 
        :param new_kind: New file-kind of the file
545
 
        """
546
 
        if None in (old_kind, new_kind):
547
 
            return DiffPath.CANNOT_DIFF
548
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
549
 
                                       new_path, old_kind, None)
550
 
        if result is DiffPath.CANNOT_DIFF:
551
 
            return result
552
 
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
553
 
                                     None, new_kind)
554
 
 
555
 
 
556
 
class DiffDirectory(DiffPath):
557
 
 
558
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
559
 
        """Perform comparison between two directories.  (dummy)
560
 
 
561
 
        """
562
 
        if 'directory' not in (old_kind, new_kind):
563
 
            return self.CANNOT_DIFF
564
 
        if old_kind not in ('directory', None):
565
 
            return self.CANNOT_DIFF
566
 
        if new_kind not in ('directory', None):
567
 
            return self.CANNOT_DIFF
568
 
        return self.CHANGED
569
 
 
570
 
 
571
 
class DiffSymlink(DiffPath):
572
 
 
573
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
574
 
        """Perform comparison between two symlinks
575
 
 
576
 
        :param file_id: The file_id of the file to compare
577
 
        :param old_path: Path of the file in the old tree
578
 
        :param new_path: Path of the file in the new tree
579
 
        :param old_kind: Old file-kind of the file
580
 
        :param new_kind: New file-kind of the file
581
 
        """
582
 
        if 'symlink' not in (old_kind, new_kind):
583
 
            return self.CANNOT_DIFF
584
 
        if old_kind == 'symlink':
585
 
            old_target = self.old_tree.get_symlink_target(file_id)
586
 
        elif old_kind is None:
587
 
            old_target = None
588
 
        else:
589
 
            return self.CANNOT_DIFF
590
 
        if new_kind == 'symlink':
591
 
            new_target = self.new_tree.get_symlink_target(file_id)
592
 
        elif new_kind is None:
593
 
            new_target = None
594
 
        else:
595
 
            return self.CANNOT_DIFF
596
 
        return self.diff_symlink(old_target, new_target)
597
 
 
598
 
    def diff_symlink(self, old_target, new_target):
599
 
        if old_target is None:
600
 
            self.to_file.write('=== target is %r\n' % new_target)
601
 
        elif new_target is None:
602
 
            self.to_file.write('=== target was %r\n' % old_target)
603
 
        else:
604
 
            self.to_file.write('=== target changed %r => %r\n' %
605
 
                              (old_target, new_target))
606
 
        return self.CHANGED
607
 
 
608
 
 
609
 
class DiffText(DiffPath):
610
 
 
611
 
    # GNU Patch uses the epoch date to detect files that are being added
612
 
    # or removed in a diff.
613
 
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
614
 
 
615
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
616
 
                 old_label='', new_label='', text_differ=internal_diff):
617
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
618
 
        self.text_differ = text_differ
619
 
        self.old_label = old_label
620
 
        self.new_label = new_label
621
 
        self.path_encoding = path_encoding
622
 
 
623
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
624
 
        """Compare two files in unified diff format
625
 
 
626
 
        :param file_id: The file_id of the file to compare
627
 
        :param old_path: Path of the file in the old tree
628
 
        :param new_path: Path of the file in the new tree
629
 
        :param old_kind: Old file-kind of the file
630
 
        :param new_kind: New file-kind of the file
631
 
        """
632
 
        if 'file' not in (old_kind, new_kind):
633
 
            return self.CANNOT_DIFF
634
 
        from_file_id = to_file_id = file_id
635
 
        if old_kind == 'file':
636
 
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
637
 
        elif old_kind is None:
638
 
            old_date = self.EPOCH_DATE
639
 
            from_file_id = None
640
 
        else:
641
 
            return self.CANNOT_DIFF
642
 
        if new_kind == 'file':
643
 
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
644
 
        elif new_kind is None:
645
 
            new_date = self.EPOCH_DATE
646
 
            to_file_id = None
647
 
        else:
648
 
            return self.CANNOT_DIFF
649
 
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
650
 
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
651
 
        return self.diff_text(from_file_id, to_file_id, from_label, to_label)
652
 
 
653
 
    def diff_text(self, from_file_id, to_file_id, from_label, to_label):
654
 
        """Diff the content of given files in two trees
655
 
 
656
 
        :param from_file_id: The id of the file in the from tree.  If None,
657
 
            the file is not present in the from tree.
658
 
        :param to_file_id: The id of the file in the to tree.  This may refer
659
 
            to a different file from from_file_id.  If None,
660
 
            the file is not present in the to tree.
661
 
        """
662
 
        def _get_text(tree, file_id):
663
 
            if file_id is not None:
664
 
                return tree.get_file(file_id).readlines()
665
 
            else:
666
 
                return []
667
 
        try:
668
 
            from_text = _get_text(self.old_tree, from_file_id)
669
 
            to_text = _get_text(self.new_tree, to_file_id)
670
 
            self.text_differ(from_label, from_text, to_label, to_text,
671
 
                             self.to_file)
672
 
        except errors.BinaryFile:
673
 
            self.to_file.write(
674
 
                  ("Binary files %s and %s differ\n" %
675
 
                  (from_label, to_label)).encode(self.path_encoding))
676
 
        return self.CHANGED
677
 
 
678
 
 
679
 
class DiffFromTool(DiffPath):
680
 
 
681
 
    def __init__(self, command_template, old_tree, new_tree, to_file,
682
 
                 path_encoding='utf-8'):
683
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
684
 
        self.command_template = command_template
685
 
        self._root = tempfile.mkdtemp(prefix='bzr-diff-')
686
 
 
687
 
    @classmethod
688
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
689
 
                    path_encoding='utf-8'):
690
 
        command_template = commands.shlex_split_unicode(command_string)
691
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
692
 
        return klass(command_template, old_tree, new_tree, to_file,
693
 
                     path_encoding)
694
 
 
695
 
    @classmethod
696
 
    def make_from_diff_tree(klass, command_string):
697
 
        def from_diff_tree(diff_tree):
698
 
            return klass.from_string(command_string, diff_tree.old_tree,
699
 
                                     diff_tree.new_tree, diff_tree.to_file)
700
 
        return from_diff_tree
701
 
 
702
 
    def _get_command(self, old_path, new_path):
703
 
        my_map = {'old_path': old_path, 'new_path': new_path}
704
 
        return [t % my_map for t in self.command_template]
705
 
 
706
 
    def _execute(self, old_path, new_path):
707
 
        command = self._get_command(old_path, new_path)
708
 
        try:
709
 
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
710
 
                                    cwd=self._root)
711
 
        except OSError, e:
712
 
            if e.errno == errno.ENOENT:
713
 
                raise errors.ExecutableMissing(command[0])
714
 
            else:
715
 
                raise
716
 
        self.to_file.write(proc.stdout.read())
717
 
        return proc.wait()
718
 
 
719
 
    def _try_symlink_root(self, tree, prefix):
720
 
        if (getattr(tree, 'abspath', None) is None
721
 
            or not osutils.host_os_dereferences_symlinks()):
722
 
            return False
723
 
        try:
724
 
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
725
 
        except OSError, e:
726
 
            if e.errno != errno.EEXIST:
727
 
                raise
728
 
        return True
729
 
 
730
 
    def _write_file(self, file_id, tree, prefix, relpath):
731
 
        full_path = osutils.pathjoin(self._root, prefix, relpath)
732
 
        if self._try_symlink_root(tree, prefix):
733
 
            return full_path
734
 
        parent_dir = osutils.dirname(full_path)
735
 
        try:
736
 
            os.makedirs(parent_dir)
737
 
        except OSError, e:
738
 
            if e.errno != errno.EEXIST:
739
 
                raise
740
 
        source = tree.get_file(file_id, relpath)
741
 
        try:
742
 
            target = open(full_path, 'wb')
743
 
            try:
744
 
                osutils.pumpfile(source, target)
745
 
            finally:
746
 
                target.close()
747
 
        finally:
748
 
            source.close()
749
 
        osutils.make_readonly(full_path)
750
 
        mtime = tree.get_file_mtime(file_id)
751
 
        os.utime(full_path, (mtime, mtime))
752
 
        return full_path
753
 
 
754
 
    def _prepare_files(self, file_id, old_path, new_path):
755
 
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
756
 
                                         old_path)
757
 
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
758
 
                                         new_path)
759
 
        return old_disk_path, new_disk_path
760
 
 
761
 
    def finish(self):
762
 
        osutils.rmtree(self._root)
763
 
 
764
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
765
 
        if (old_kind, new_kind) != ('file', 'file'):
766
 
            return DiffPath.CANNOT_DIFF
767
 
        self._prepare_files(file_id, old_path, new_path)
768
 
        self._execute(osutils.pathjoin('old', old_path),
769
 
                      osutils.pathjoin('new', new_path))
770
 
 
771
 
 
772
 
class DiffTree(object):
773
 
    """Provides textual representations of the difference between two trees.
774
 
 
775
 
    A DiffTree examines two trees and where a file-id has altered
776
 
    between them, generates a textual representation of the difference.
777
 
    DiffTree uses a sequence of DiffPath objects which are each
778
 
    given the opportunity to handle a given altered fileid. The list
779
 
    of DiffPath objects can be extended globally by appending to
780
 
    DiffTree.diff_factories, or for a specific diff operation by
781
 
    supplying the extra_factories option to the appropriate method.
782
 
    """
783
 
 
784
 
    # list of factories that can provide instances of DiffPath objects
785
 
    # may be extended by plugins.
786
 
    diff_factories = [DiffSymlink.from_diff_tree,
787
 
                      DiffDirectory.from_diff_tree]
788
 
 
789
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
790
 
                 diff_text=None, extra_factories=None):
791
 
        """Constructor
792
 
 
793
 
        :param old_tree: Tree to show as old in the comparison
794
 
        :param new_tree: Tree to show as new in the comparison
795
 
        :param to_file: File to write comparision to
796
 
        :param path_encoding: Character encoding to write paths in
797
 
        :param diff_text: DiffPath-type object to use as a last resort for
798
 
            diffing text files.
799
 
        :param extra_factories: Factories of DiffPaths to try before any other
800
 
            DiffPaths"""
801
 
        if diff_text is None:
802
 
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
803
 
                                 '', '',  internal_diff)
804
 
        self.old_tree = old_tree
805
 
        self.new_tree = new_tree
806
 
        self.to_file = to_file
807
 
        self.path_encoding = path_encoding
808
 
        self.differs = []
809
 
        if extra_factories is not None:
810
 
            self.differs.extend(f(self) for f in extra_factories)
811
 
        self.differs.extend(f(self) for f in self.diff_factories)
812
 
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
813
 
 
814
 
    @classmethod
815
 
    def from_trees_options(klass, old_tree, new_tree, to_file,
816
 
                           path_encoding, external_diff_options, old_label,
817
 
                           new_label, using):
818
 
        """Factory for producing a DiffTree.
819
 
 
820
 
        Designed to accept options used by show_diff_trees.
821
 
        :param old_tree: The tree to show as old in the comparison
822
 
        :param new_tree: The tree to show as new in the comparison
823
 
        :param to_file: File to write comparisons to
824
 
        :param path_encoding: Character encoding to use for writing paths
825
 
        :param external_diff_options: If supplied, use the installed diff
826
 
            binary to perform file comparison, using supplied options.
827
 
        :param old_label: Prefix to use for old file labels
828
 
        :param new_label: Prefix to use for new file labels
829
 
        :param using: Commandline to use to invoke an external diff tool
830
 
        """
831
 
        if using is not None:
832
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using)]
833
 
        else:
834
 
            extra_factories = []
835
 
        if external_diff_options:
836
 
            opts = external_diff_options.split()
837
 
            def diff_file(olab, olines, nlab, nlines, to_file):
838
 
                external_diff(olab, olines, nlab, nlines, to_file, opts)
839
 
        else:
840
 
            diff_file = internal_diff
841
 
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
842
 
                             old_label, new_label, diff_file)
843
 
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
844
 
                     extra_factories)
845
 
 
846
 
    def show_diff(self, specific_files, extra_trees=None):
847
 
        """Write tree diff to self.to_file
848
 
 
849
 
        :param sepecific_files: the specific files to compare (recursive)
850
 
        :param extra_trees: extra trees to use for mapping paths to file_ids
851
 
        """
852
 
        try:
853
 
            return self._show_diff(specific_files, extra_trees)
854
 
        finally:
855
 
            for differ in self.differs:
856
 
                differ.finish()
857
 
 
858
 
    def _show_diff(self, specific_files, extra_trees):
859
 
        # TODO: Generation of pseudo-diffs for added/deleted files could
860
 
        # be usefully made into a much faster special case.
861
 
        iterator = self.new_tree.iter_changes(self.old_tree,
862
 
                                               specific_files=specific_files,
863
 
                                               extra_trees=extra_trees,
864
 
                                               require_versioned=True)
865
 
        has_changes = 0
866
 
        def changes_key(change):
867
 
            old_path, new_path = change[1]
868
 
            path = new_path
869
 
            if path is None:
870
 
                path = old_path
871
 
            return path
872
 
        def get_encoded_path(path):
873
 
            if path is not None:
874
 
                return path.encode(self.path_encoding, "replace")
875
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
876
 
             executable) in sorted(iterator, key=changes_key):
877
 
            if parent == (None, None):
878
 
                continue
879
 
            oldpath, newpath = paths
880
 
            oldpath_encoded = get_encoded_path(paths[0])
881
 
            newpath_encoded = get_encoded_path(paths[1])
882
 
            old_present = (kind[0] is not None and versioned[0])
883
 
            new_present = (kind[1] is not None and versioned[1])
884
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
885
 
 
886
 
            properties_changed = []
887
 
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
888
 
 
889
 
            if properties_changed:
890
 
                prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
891
 
            else:
892
 
                prop_str = ""
893
 
 
894
 
            if (old_present, new_present) == (True, False):
895
 
                self.to_file.write("=== removed %s '%s'\n" %
896
 
                                   (kind[0], oldpath_encoded))
897
 
                newpath = oldpath
898
 
            elif (old_present, new_present) == (False, True):
899
 
                self.to_file.write("=== added %s '%s'\n" %
900
 
                                   (kind[1], newpath_encoded))
901
 
                oldpath = newpath
902
 
            elif renamed:
903
 
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
904
 
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
905
 
            else:
906
 
                # if it was produced by iter_changes, it must be
907
 
                # modified *somehow*, either content or execute bit.
908
 
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
909
 
                                   newpath_encoded, prop_str))
910
 
            if changed_content:
911
 
                self.diff(file_id, oldpath, newpath)
912
 
                has_changes = 1
913
 
            if renamed:
914
 
                has_changes = 1
915
 
        return has_changes
916
 
 
917
 
    def diff(self, file_id, old_path, new_path):
918
 
        """Perform a diff of a single file
919
 
 
920
 
        :param file_id: file-id of the file
921
 
        :param old_path: The path of the file in the old tree
922
 
        :param new_path: The path of the file in the new tree
923
 
        """
924
 
        try:
925
 
            old_kind = self.old_tree.kind(file_id)
926
 
        except (errors.NoSuchId, errors.NoSuchFile):
927
 
            old_kind = None
928
 
        try:
929
 
            new_kind = self.new_tree.kind(file_id)
930
 
        except (errors.NoSuchId, errors.NoSuchFile):
931
 
            new_kind = None
932
 
 
933
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
934
 
                                       new_path, old_kind, new_kind)
935
 
        if result is DiffPath.CANNOT_DIFF:
936
 
            error_path = new_path
937
 
            if error_path is None:
938
 
                error_path = old_path
939
 
            raise errors.NoDiffFound(error_path)
 
516
 
 
517
def _maybe_diff_file_or_symlink(old_path, old_tree, file_id,
 
518
                                new_path, new_tree, text_modified,
 
519
                                kind, to_file, diff_file):
 
520
    if text_modified:
 
521
        new_entry = new_tree.inventory[file_id]
 
522
        old_tree.inventory[file_id].diff(diff_file,
 
523
                                         old_path, old_tree,
 
524
                                         new_path, new_entry, 
 
525
                                         new_tree, to_file)