/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 breezy/diff.py

Merge test-run support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
import difflib
20
20
import os
21
21
import re
 
22
import string
22
23
import sys
23
24
 
24
25
from .lazy_import import lazy_import
25
26
lazy_import(globals(), """
26
27
import errno
27
 
import patiencediff
28
28
import subprocess
29
29
import tempfile
30
30
 
31
31
from breezy import (
32
32
    cleanup,
 
33
    cmdline,
33
34
    controldir,
 
35
    errors,
34
36
    osutils,
 
37
    patiencediff,
35
38
    textfile,
36
39
    timestamp,
37
40
    views,
41
44
from breezy.i18n import gettext
42
45
""")
43
46
 
44
 
from . import (
45
 
    errors,
46
 
    )
47
47
from .registry import (
48
48
    Registry,
49
49
    )
50
 
from .sixish import text_type
51
50
from .trace import mutter, note, warning
52
51
from .tree import FileTimestampUnavailable
53
52
 
54
53
 
55
54
DEFAULT_CONTEXT_AMOUNT = 3
56
55
 
 
56
class AtTemplate(string.Template):
 
57
    """Templating class that uses @ instead of $."""
 
58
 
 
59
    delimiter = '@'
 
60
 
57
61
 
58
62
# TODO: Rather than building a changeset object, we should probably
59
63
# invoke callbacks on an object.  That object can either accumulate a
69
73
        self.opcodes = None
70
74
 
71
75
 
72
 
def internal_diff(old_label, oldlines, new_label, newlines, to_file,
 
76
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
73
77
                  allow_binary=False, sequence_matcher=None,
74
78
                  path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
75
79
    # FIXME: difflib is wrong if there is no trailing newline.
88
92
 
89
93
    if sequence_matcher is None:
90
94
        sequence_matcher = patiencediff.PatienceSequenceMatcher
91
 
    ud = unified_diff_bytes(
92
 
        oldlines, newlines,
93
 
        fromfile=old_label.encode(path_encoding, 'replace'),
94
 
        tofile=new_label.encode(path_encoding, 'replace'),
95
 
        n=context_lines, sequencematcher=sequence_matcher)
 
95
    ud = patiencediff.unified_diff(oldlines, newlines,
 
96
                      fromfile=old_filename.encode(path_encoding, 'replace'),
 
97
                      tofile=new_filename.encode(path_encoding, 'replace'),
 
98
                      n=context_lines, sequencematcher=sequence_matcher)
96
99
 
97
100
    ud = list(ud)
98
 
    if len(ud) == 0:  # Identical contents, nothing to do
 
101
    if len(ud) == 0: # Identical contents, nothing to do
99
102
        return
100
103
    # work-around for difflib being too smart for its own good
101
104
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
102
105
    if not oldlines:
103
 
        ud[2] = ud[2].replace(b'-1,0', b'-0,0')
 
106
        ud[2] = ud[2].replace('-1,0', '-0,0')
104
107
    elif not newlines:
105
 
        ud[2] = ud[2].replace(b'+1,0', b'+0,0')
 
108
        ud[2] = ud[2].replace('+1,0', '+0,0')
106
109
 
107
110
    for line in ud:
108
111
        to_file.write(line)
109
 
        if not line.endswith(b'\n'):
110
 
            to_file.write(b"\n\\ No newline at end of file\n")
111
 
    to_file.write(b'\n')
112
 
 
113
 
 
114
 
def unified_diff_bytes(a, b, fromfile=b'', tofile=b'', fromfiledate=b'',
115
 
                       tofiledate=b'', n=3, lineterm=b'\n', sequencematcher=None):
116
 
    r"""
117
 
    Compare two sequences of lines; generate the delta as a unified diff.
118
 
 
119
 
    Unified diffs are a compact way of showing line changes and a few
120
 
    lines of context.  The number of context lines is set by 'n' which
121
 
    defaults to three.
122
 
 
123
 
    By default, the diff control lines (those with ---, +++, or @@) are
124
 
    created with a trailing newline.  This is helpful so that inputs
125
 
    created from file.readlines() result in diffs that are suitable for
126
 
    file.writelines() since both the inputs and outputs have trailing
127
 
    newlines.
128
 
 
129
 
    For inputs that do not have trailing newlines, set the lineterm
130
 
    argument to "" so that the output will be uniformly newline free.
131
 
 
132
 
    The unidiff format normally has a header for filenames and modification
133
 
    times.  Any or all of these may be specified using strings for
134
 
    'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.  The modification
135
 
    times are normally expressed in the format returned by time.ctime().
136
 
 
137
 
    Example:
138
 
 
139
 
    >>> for line in bytes_unified_diff(b'one two three four'.split(),
140
 
    ...             b'zero one tree four'.split(), b'Original', b'Current',
141
 
    ...             b'Sat Jan 26 23:30:50 1991', b'Fri Jun 06 10:20:52 2003',
142
 
    ...             lineterm=b''):
143
 
    ...     print line
144
 
    --- Original Sat Jan 26 23:30:50 1991
145
 
    +++ Current Fri Jun 06 10:20:52 2003
146
 
    @@ -1,4 +1,4 @@
147
 
    +zero
148
 
     one
149
 
    -two
150
 
    -three
151
 
    +tree
152
 
     four
153
 
    """
154
 
    if sequencematcher is None:
155
 
        sequencematcher = difflib.SequenceMatcher
156
 
 
157
 
    if fromfiledate:
158
 
        fromfiledate = b'\t' + bytes(fromfiledate)
159
 
    if tofiledate:
160
 
        tofiledate = b'\t' + bytes(tofiledate)
161
 
 
162
 
    started = False
163
 
    for group in sequencematcher(None, a, b).get_grouped_opcodes(n):
164
 
        if not started:
165
 
            yield b'--- %s%s%s' % (fromfile, fromfiledate, lineterm)
166
 
            yield b'+++ %s%s%s' % (tofile, tofiledate, lineterm)
167
 
            started = True
168
 
        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
169
 
        yield b"@@ -%d,%d +%d,%d @@%s" % (i1 + 1, i2 - i1, j1 + 1, j2 - j1, lineterm)
170
 
        for tag, i1, i2, j1, j2 in group:
171
 
            if tag == 'equal':
172
 
                for line in a[i1:i2]:
173
 
                    yield b' ' + line
174
 
                continue
175
 
            if tag == 'replace' or tag == 'delete':
176
 
                for line in a[i1:i2]:
177
 
                    yield b'-' + line
178
 
            if tag == 'replace' or tag == 'insert':
179
 
                for line in b[j1:j2]:
180
 
                    yield b'+' + line
 
112
        if not line.endswith('\n'):
 
113
            to_file.write("\n\\ No newline at end of file\n")
 
114
    to_file.write('\n')
181
115
 
182
116
 
183
117
def _spawn_external_diff(diffcmd, capture_errors=True):
216
150
 
217
151
    return pipe
218
152
 
219
 
 
220
153
# diff style options as of GNU diff v3.2
221
154
style_option_list = ['-c', '-C', '--context',
222
155
                     '-e', '--ed',
228
161
                     '-y', '--side-by-side',
229
162
                     '-D', '--ifdef']
230
163
 
231
 
 
232
164
def default_style_unified(diff_opts):
233
165
    """Default to unified diff style if alternative not specified in diff_opts.
234
166
 
253
185
    return diff_opts
254
186
 
255
187
 
256
 
def external_diff(old_label, oldlines, new_label, newlines, to_file,
 
188
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
257
189
                  diff_opts):
258
190
    """Display a diff by calling out to the external diff program."""
259
191
    # make sure our own output is properly ordered before the diff
283
215
        if sys.platform == 'win32':
284
216
            # Popen doesn't do the proper encoding for external commands
285
217
            # Since we are dealing with an ANSI api, use mbcs encoding
286
 
            old_label = old_label.encode('mbcs')
287
 
            new_label = new_label.encode('mbcs')
 
218
            old_filename = old_filename.encode('mbcs')
 
219
            new_filename = new_filename.encode('mbcs')
288
220
        diffcmd = ['diff',
289
 
                   '--label', old_label,
 
221
                   '--label', old_filename,
290
222
                   old_abspath,
291
 
                   '--label', new_label,
 
223
                   '--label', new_filename,
292
224
                   new_abspath,
293
225
                   '--binary',
294
 
                   ]
 
226
                  ]
295
227
 
296
228
        diff_opts = default_style_unified(diff_opts)
297
229
 
303
235
        rc = pipe.returncode
304
236
 
305
237
        # internal_diff() adds a trailing newline, add one here for consistency
306
 
        out += b'\n'
 
238
        out += '\n'
307
239
        if rc == 2:
308
240
            # 'diff' gives retcode == 2 for all sorts of errors
309
241
            # one of those is 'Binary files differ'.
316
248
            out, err = pipe.communicate()
317
249
 
318
250
            # Write out the new i18n diff response
319
 
            to_file.write(out + b'\n')
 
251
            to_file.write(out+'\n')
320
252
            if pipe.returncode != 2:
321
253
                raise errors.BzrError(
322
 
                    'external diff failed with exit code 2'
323
 
                    ' when run with LANG=C and LC_ALL=C,'
324
 
                    ' but not when run natively: %r' % (diffcmd,))
 
254
                               'external diff failed with exit code 2'
 
255
                               ' when run with LANG=C and LC_ALL=C,'
 
256
                               ' but not when run natively: %r' % (diffcmd,))
325
257
 
326
 
            first_line = lang_c_out.split(b'\n', 1)[0]
 
258
            first_line = lang_c_out.split('\n', 1)[0]
327
259
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
328
 
            m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
 
260
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
329
261
            if m is None:
330
262
                raise errors.BzrError('external diff failed with exit code 2;'
331
263
                                      ' command: %r' % (diffcmd,))
346
278
            raise errors.BzrError('external diff failed with %s; command: %r'
347
279
                                  % (msg, diffcmd))
348
280
 
 
281
 
349
282
    finally:
350
283
        oldtmpf.close()                 # and delete
351
284
        newtmpf.close()
365
298
 
366
299
 
367
300
def get_trees_and_branches_to_diff_locked(
368
 
        path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
 
301
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
369
302
    """Get the trees and specific files to diff given a list of paths.
370
303
 
371
304
    This method works out the trees to be diff'ed and the files of
382
315
    :param new_url:
383
316
        The url of the new branch or tree. If None, the tree to use is
384
317
        taken from the first path, if any, or the current working tree.
385
 
    :param exit_stack:
386
 
        an ExitStack object. get_trees_and_branches_to_diff
 
318
    :param add_cleanup:
 
319
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
387
320
        will register cleanups that must be run to unlock the trees, etc.
388
321
    :param apply_view:
389
322
        if True and a view is set, apply the view or check that the paths
392
325
        a tuple of (old_tree, new_tree, old_branch, new_branch,
393
326
        specific_files, extra_trees) where extra_trees is a sequence of
394
327
        additional trees to search in for file-ids.  The trees and branches
395
 
        will be read-locked until the cleanups registered via the exit_stack
 
328
        will be read-locked until the cleanups registered via the add_cleanup
396
329
        param are run.
397
330
    """
398
331
    # Get the old and new revision specs
424
357
 
425
358
    def lock_tree_or_branch(wt, br):
426
359
        if wt is not None:
427
 
            exit_stack.enter_context(wt.lock_read())
 
360
            wt.lock_read()
 
361
            add_cleanup(wt.unlock)
428
362
        elif br is not None:
429
 
            exit_stack.enter_context(br.lock_read())
 
363
            br.lock_read()
 
364
            add_cleanup(br.unlock)
430
365
 
431
366
    # Get the old location
432
367
    specific_files = []
454
389
                views.check_path_in_view(working_tree, relpath)
455
390
            specific_files.append(relpath)
456
391
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
457
 
                                 basis_is_default=working_tree is None)
 
392
        basis_is_default=working_tree is None)
458
393
    new_branch = branch
459
394
 
460
395
    # Get the specific files (all files is None, no files is [])
465
400
    specific_files.extend(other_paths)
466
401
    if len(specific_files) == 0:
467
402
        specific_files = None
468
 
        if (working_tree is not None and working_tree.supports_views() and
469
 
                apply_view):
 
403
        if (working_tree is not None and working_tree.supports_views()
 
404
            and apply_view):
470
405
            view_files = working_tree.views.lookup_view()
471
406
            if view_files:
472
407
                specific_files = view_files
508
443
    :param to_file: The output stream.
509
444
    :param specific_files: Include only changes to these files - None for all
510
445
        changes.
511
 
    :param external_diff_options: If set, use an external GNU diff and pass
 
446
    :param external_diff_options: If set, use an external GNU diff and pass 
512
447
        these options.
513
448
    :param extra_trees: If set, more Trees to use for looking up file ids
514
 
    :param path_encoding: If set, the path will be encoded as specified,
 
449
    :param path_encoding: If set, the path will be encoded as specified, 
515
450
        otherwise is supposed to be utf8
516
451
    :param format_cls: Formatter class (DiffTree subclass)
517
452
    """
519
454
        context = DEFAULT_CONTEXT_AMOUNT
520
455
    if format_cls is None:
521
456
        format_cls = DiffTree
522
 
    with cleanup.ExitStack() as exit_stack:
523
 
        exit_stack.enter_context(old_tree.lock_read())
 
457
    with old_tree.lock_read():
524
458
        if extra_trees is not None:
525
459
            for tree in extra_trees:
526
 
                exit_stack.enter_context(tree.lock_read())
527
 
        exit_stack.enter_context(new_tree.lock_read())
528
 
        differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
529
 
                                               path_encoding,
530
 
                                               external_diff_options,
531
 
                                               old_label, new_label, using,
532
 
                                               context_lines=context)
533
 
        return differ.show_diff(specific_files, extra_trees)
534
 
 
535
 
 
536
 
def _patch_header_date(tree, path):
 
460
                tree.lock_read()
 
461
        new_tree.lock_read()
 
462
        try:
 
463
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
464
                                                   path_encoding,
 
465
                                                   external_diff_options,
 
466
                                                   old_label, new_label, using,
 
467
                                                   context_lines=context)
 
468
            return differ.show_diff(specific_files, extra_trees)
 
469
        finally:
 
470
            new_tree.unlock()
 
471
            if extra_trees is not None:
 
472
                for tree in extra_trees:
 
473
                    tree.unlock()
 
474
 
 
475
 
 
476
def _patch_header_date(tree, file_id, path):
537
477
    """Returns a timestamp suitable for use in a patch header."""
538
478
    try:
539
 
        mtime = tree.get_file_mtime(path)
 
479
        mtime = tree.get_file_mtime(path, file_id)
540
480
    except FileTimestampUnavailable:
541
481
        mtime = 0
542
482
    return timestamp.format_patch_date(mtime)
543
483
 
544
484
 
545
485
def get_executable_change(old_is_x, new_is_x):
546
 
    descr = {True: b"+x", False: b"-x", None: b"??"}
 
486
    descr = { True:"+x", False:"-x", None:"??" }
547
487
    if old_is_x != new_is_x:
548
 
        return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
 
488
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
549
489
    else:
550
490
        return []
551
491
 
582
522
                     diff_tree.to_file, diff_tree.path_encoding)
583
523
 
584
524
    @staticmethod
585
 
    def _diff_many(differs, old_path, new_path, old_kind, new_kind):
 
525
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
586
526
        for file_differ in differs:
587
 
            result = file_differ.diff(old_path, new_path, old_kind, new_kind)
 
527
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
 
528
                                      new_kind)
588
529
            if result is not DiffPath.CANNOT_DIFF:
589
530
                return result
590
531
        else:
597
538
    Represents kind change as deletion + creation.  Uses the other differs
598
539
    to do this.
599
540
    """
600
 
 
601
541
    def __init__(self, differs):
602
542
        self.differs = differs
603
543
 
608
548
    def from_diff_tree(klass, diff_tree):
609
549
        return klass(diff_tree.differs)
610
550
 
611
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
551
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
612
552
        """Perform comparison
613
553
 
 
554
        :param file_id: The file_id of the file to compare
614
555
        :param old_path: Path of the file in the old tree
615
556
        :param new_path: Path of the file in the new tree
616
557
        :param old_kind: Old file-kind of the file
618
559
        """
619
560
        if None in (old_kind, new_kind):
620
561
            return DiffPath.CANNOT_DIFF
621
 
        result = DiffPath._diff_many(
622
 
            self.differs, old_path, new_path, old_kind, None)
 
562
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
563
                                       new_path, old_kind, None)
623
564
        if result is DiffPath.CANNOT_DIFF:
624
565
            return result
625
 
        return DiffPath._diff_many(
626
 
            self.differs, old_path, new_path, None, new_kind)
627
 
 
628
 
 
629
 
class DiffTreeReference(DiffPath):
630
 
 
631
 
    def diff(self, old_path, new_path, old_kind, new_kind):
632
 
        """Perform comparison between two tree references.  (dummy)
633
 
 
634
 
        """
635
 
        if 'tree-reference' not in (old_kind, new_kind):
636
 
            return self.CANNOT_DIFF
637
 
        if old_kind not in ('tree-reference', None):
638
 
            return self.CANNOT_DIFF
639
 
        if new_kind not in ('tree-reference', None):
640
 
            return self.CANNOT_DIFF
641
 
        return self.CHANGED
 
566
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
 
567
                                     None, new_kind)
642
568
 
643
569
 
644
570
class DiffDirectory(DiffPath):
645
571
 
646
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
572
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
647
573
        """Perform comparison between two directories.  (dummy)
648
574
 
649
575
        """
658
584
 
659
585
class DiffSymlink(DiffPath):
660
586
 
661
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
587
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
662
588
        """Perform comparison between two symlinks
663
589
 
 
590
        :param file_id: The file_id of the file to compare
664
591
        :param old_path: Path of the file in the old tree
665
592
        :param new_path: Path of the file in the new tree
666
593
        :param old_kind: Old file-kind of the file
669
596
        if 'symlink' not in (old_kind, new_kind):
670
597
            return self.CANNOT_DIFF
671
598
        if old_kind == 'symlink':
672
 
            old_target = self.old_tree.get_symlink_target(old_path)
 
599
            old_target = self.old_tree.get_symlink_target(old_path, file_id)
673
600
        elif old_kind is None:
674
601
            old_target = None
675
602
        else:
676
603
            return self.CANNOT_DIFF
677
604
        if new_kind == 'symlink':
678
 
            new_target = self.new_tree.get_symlink_target(new_path)
 
605
            new_target = self.new_tree.get_symlink_target(new_path, file_id)
679
606
        elif new_kind is None:
680
607
            new_target = None
681
608
        else:
684
611
 
685
612
    def diff_symlink(self, old_target, new_target):
686
613
        if old_target is None:
687
 
            self.to_file.write(b'=== target is \'%s\'\n' %
688
 
                               new_target.encode(self.path_encoding, 'replace'))
 
614
            self.to_file.write('=== target is %r\n' % new_target)
689
615
        elif new_target is None:
690
 
            self.to_file.write(b'=== target was \'%s\'\n' %
691
 
                               old_target.encode(self.path_encoding, 'replace'))
 
616
            self.to_file.write('=== target was %r\n' % old_target)
692
617
        else:
693
 
            self.to_file.write(b'=== target changed \'%s\' => \'%s\'\n' %
694
 
                               (old_target.encode(self.path_encoding, 'replace'),
695
 
                                new_target.encode(self.path_encoding, 'replace')))
 
618
            self.to_file.write('=== target changed %r => %r\n' %
 
619
                              (old_target, new_target))
696
620
        return self.CHANGED
697
621
 
698
622
 
702
626
    # or removed in a diff.
703
627
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
704
628
 
705
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
706
 
                 old_label='', new_label='', text_differ=internal_diff,
 
629
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8', 
 
630
                 old_label='', new_label='', text_differ=internal_diff, 
707
631
                 context_lines=DEFAULT_CONTEXT_AMOUNT):
708
632
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
709
633
        self.text_differ = text_differ
712
636
        self.path_encoding = path_encoding
713
637
        self.context_lines = context_lines
714
638
 
715
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
639
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
716
640
        """Compare two files in unified diff format
717
641
 
 
642
        :param file_id: The file_id of the file to compare
718
643
        :param old_path: Path of the file in the old tree
719
644
        :param new_path: Path of the file in the new tree
720
645
        :param old_kind: Old file-kind of the file
722
647
        """
723
648
        if 'file' not in (old_kind, new_kind):
724
649
            return self.CANNOT_DIFF
 
650
        from_file_id = to_file_id = file_id
725
651
        if old_kind == 'file':
726
 
            old_date = _patch_header_date(self.old_tree, old_path)
 
652
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
727
653
        elif old_kind is None:
728
654
            old_date = self.EPOCH_DATE
 
655
            from_file_id = None
729
656
        else:
730
657
            return self.CANNOT_DIFF
731
658
        if new_kind == 'file':
732
 
            new_date = _patch_header_date(self.new_tree, new_path)
 
659
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
733
660
        elif new_kind is None:
734
661
            new_date = self.EPOCH_DATE
 
662
            to_file_id = None
735
663
        else:
736
664
            return self.CANNOT_DIFF
737
 
        from_label = '%s%s\t%s' % (self.old_label, old_path,
738
 
                                   old_date)
739
 
        to_label = '%s%s\t%s' % (self.new_label, new_path,
740
 
                                 new_date)
741
 
        return self.diff_text(old_path, new_path, from_label, to_label)
 
665
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
 
666
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
 
667
        return self.diff_text(old_path, new_path, from_label, to_label,
 
668
            from_file_id, to_file_id)
742
669
 
743
 
    def diff_text(self, from_path, to_path, from_label, to_label):
 
670
    def diff_text(self, from_path, to_path, from_label, to_label,
 
671
        from_file_id=None, to_file_id=None):
744
672
        """Diff the content of given files in two trees
745
673
 
746
674
        :param from_path: The path in the from tree. If None,
748
676
        :param to_path: The path in the to tree. This may refer
749
677
            to a different file from from_path.  If None,
750
678
            the file is not present in the to tree.
 
679
        :param from_file_id: The id of the file in the from tree or None if
 
680
            unknown.
 
681
        :param to_file_id: The id of the file in the to tree or None if
 
682
            unknown.
751
683
        """
752
 
        def _get_text(tree, path):
753
 
            if path is None:
754
 
                return []
755
 
            try:
756
 
                return tree.get_file_lines(path)
757
 
            except errors.NoSuchFile:
758
 
                return []
 
684
        def _get_text(tree, file_id, path):
 
685
            if file_id is None:
 
686
                return []
 
687
            return tree.get_file_lines(path, file_id)
759
688
        try:
760
 
            from_text = _get_text(self.old_tree, from_path)
761
 
            to_text = _get_text(self.new_tree, to_path)
 
689
            from_text = _get_text(self.old_tree, from_file_id, from_path)
 
690
            to_text = _get_text(self.new_tree, to_file_id, to_path)
762
691
            self.text_differ(from_label, from_text, to_label, to_text,
763
692
                             self.to_file, path_encoding=self.path_encoding,
764
693
                             context_lines=self.context_lines)
765
694
        except errors.BinaryFile:
766
695
            self.to_file.write(
767
 
                ("Binary files %s%s and %s%s differ\n" %
768
 
                 (self.old_label, from_path, self.new_label, to_path)).encode(self.path_encoding, 'replace'))
 
696
                  ("Binary files %s and %s differ\n" %
 
697
                  (from_label, to_label)).encode(self.path_encoding, 'replace'))
769
698
        return self.CHANGED
770
699
 
771
700
 
778
707
        self._root = osutils.mkdtemp(prefix='brz-diff-')
779
708
 
780
709
    @classmethod
781
 
    def from_string(klass, command_template, old_tree, new_tree, to_file,
 
710
    def from_string(klass, command_string, old_tree, new_tree, to_file,
782
711
                    path_encoding='utf-8'):
 
712
        command_template = cmdline.split(command_string)
 
713
        if '@' not in command_string:
 
714
            command_template.extend(['@old_path', '@new_path'])
783
715
        return klass(command_template, old_tree, new_tree, to_file,
784
716
                     path_encoding)
785
717
 
795
727
 
796
728
    def _get_command(self, old_path, new_path):
797
729
        my_map = {'old_path': old_path, 'new_path': new_path}
798
 
        command = [t.format(**my_map) for t in
 
730
        command = [AtTemplate(t).substitute(my_map) for t in
799
731
                   self.command_template]
800
 
        if command == self.command_template:
801
 
            command += [old_path, new_path]
802
 
        if sys.platform == 'win32':  # Popen doesn't accept unicode on win32
 
732
        if sys.platform == 'win32': # Popen doesn't accept unicode on win32
803
733
            command_encoded = []
804
734
            for c in command:
805
735
                if isinstance(c, text_type):
821
751
            else:
822
752
                raise
823
753
        self.to_file.write(proc.stdout.read())
824
 
        proc.stdout.close()
825
754
        return proc.wait()
826
755
 
827
756
    def _try_symlink_root(self, tree, prefix):
828
 
        if (getattr(tree, 'abspath', None) is None or
829
 
                not osutils.host_os_dereferences_symlinks()):
 
757
        if (getattr(tree, 'abspath', None) is None
 
758
            or not osutils.host_os_dereferences_symlinks()):
830
759
            return False
831
760
        try:
832
761
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
864
793
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
865
794
 
866
795
    def _write_file(self, relpath, tree, prefix, force_temp=False,
867
 
                    allow_write=False):
 
796
                    allow_write=False, file_id=None):
868
797
        if not force_temp and isinstance(tree, WorkingTree):
869
798
            full_path = tree.abspath(relpath)
870
799
            if self._is_safepath(full_path):
879
808
        except OSError as e:
880
809
            if e.errno != errno.EEXIST:
881
810
                raise
882
 
        with tree.get_file(relpath) as source, \
883
 
                open(full_path, 'wb') as target:
884
 
            osutils.pumpfile(source, target)
885
 
        try:
886
 
            mtime = tree.get_file_mtime(relpath)
 
811
        source = tree.get_file(relpath, file_id)
 
812
        try:
 
813
            with open(full_path, 'wb') as target:
 
814
                osutils.pumpfile(source, target)
 
815
        finally:
 
816
            source.close()
 
817
        try:
 
818
            mtime = tree.get_file_mtime(relpath, file_id)
887
819
        except FileTimestampUnavailable:
888
820
            pass
889
821
        else:
893
825
        return full_path
894
826
 
895
827
    def _prepare_files(self, old_path, new_path, force_temp=False,
896
 
                       allow_write_new=False):
897
 
        old_disk_path = self._write_file(
898
 
            old_path, self.old_tree, 'old', force_temp)
899
 
        new_disk_path = self._write_file(
900
 
            new_path, self.new_tree, 'new', force_temp,
901
 
            allow_write=allow_write_new)
 
828
                       allow_write_new=False, file_id=None):
 
829
        old_disk_path = self._write_file(old_path, self.old_tree, 'old',
 
830
                                         force_temp, file_id=file_id)
 
831
        new_disk_path = self._write_file(new_path, self.new_tree, 'new',
 
832
                                         force_temp, file_id=file_id,
 
833
                                         allow_write=allow_write_new)
902
834
        return old_disk_path, new_disk_path
903
835
 
904
836
    def finish(self):
907
839
        except OSError as e:
908
840
            if e.errno != errno.ENOENT:
909
841
                mutter("The temporary directory \"%s\" was not "
910
 
                       "cleanly removed: %s." % (self._root, e))
 
842
                        "cleanly removed: %s." % (self._root, e))
911
843
 
912
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
844
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
913
845
        if (old_kind, new_kind) != ('file', 'file'):
914
846
            return DiffPath.CANNOT_DIFF
915
847
        (old_disk_path, new_disk_path) = self._prepare_files(
916
 
            old_path, new_path)
 
848
                old_path, new_path, file_id=file_id)
917
849
        self._execute(old_disk_path, new_disk_path)
918
850
 
919
 
    def edit_file(self, old_path, new_path):
 
851
    def edit_file(self, old_path, new_path, file_id=None):
920
852
        """Use this tool to edit a file.
921
853
 
922
854
        A temporary copy will be edited, and the new contents will be
923
855
        returned.
924
856
 
 
857
        :param file_id: The id of the file to edit.
925
858
        :return: The new contents of the file.
926
859
        """
927
860
        old_abs_path, new_abs_path = self._prepare_files(
928
 
            old_path, new_path, allow_write_new=True, force_temp=True)
 
861
                old_path, new_path, allow_write_new=True, force_temp=True,
 
862
                file_id=file_id)
929
863
        command = self._get_command(old_abs_path, new_abs_path)
930
864
        subprocess.call(command, cwd=self._root)
931
865
        with open(new_abs_path, 'rb') as new_file:
947
881
    # list of factories that can provide instances of DiffPath objects
948
882
    # may be extended by plugins.
949
883
    diff_factories = [DiffSymlink.from_diff_tree,
950
 
                      DiffDirectory.from_diff_tree,
951
 
                      DiffTreeReference.from_diff_tree]
 
884
                      DiffDirectory.from_diff_tree]
952
885
 
953
886
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
954
887
                 diff_text=None, extra_factories=None):
964
897
            DiffPaths"""
965
898
        if diff_text is None:
966
899
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
967
 
                                 '', '', internal_diff)
 
900
                                 '', '',  internal_diff)
968
901
        self.old_tree = old_tree
969
902
        self.new_tree = new_tree
970
903
        self.to_file = to_file
994
927
        :param using: Commandline to use to invoke an external diff tool
995
928
        """
996
929
        if using is not None:
997
 
            extra_factories = [DiffFromTool.make_from_diff_tree(
998
 
                using, external_diff_options)]
 
930
            extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
999
931
        else:
1000
932
            extra_factories = []
1001
933
        if external_diff_options:
1002
934
            opts = external_diff_options.split()
1003
 
 
1004
935
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
1005
936
                """:param path_encoding: not used but required
1006
937
                        to match the signature of internal_diff.
1029
960
        # TODO: Generation of pseudo-diffs for added/deleted files could
1030
961
        # be usefully made into a much faster special case.
1031
962
        iterator = self.new_tree.iter_changes(self.old_tree,
1032
 
                                              specific_files=specific_files,
1033
 
                                              extra_trees=extra_trees,
1034
 
                                              require_versioned=True)
 
963
                                               specific_files=specific_files,
 
964
                                               extra_trees=extra_trees,
 
965
                                               require_versioned=True)
1035
966
        has_changes = 0
1036
 
 
1037
967
        def changes_key(change):
1038
 
            old_path, new_path = change.path
 
968
            old_path, new_path = change[1]
1039
969
            path = new_path
1040
970
            if path is None:
1041
971
                path = old_path
1042
972
            return path
1043
 
 
1044
973
        def get_encoded_path(path):
1045
974
            if path is not None:
1046
975
                return path.encode(self.path_encoding, "replace")
1047
 
        for change in sorted(iterator, key=changes_key):
 
976
        for (file_id, paths, changed_content, versioned, parent, name, kind,
 
977
             executable) in sorted(iterator, key=changes_key):
1048
978
            # The root does not get diffed, and items with no known kind (that
1049
979
            # is, missing) in both trees are skipped as well.
1050
 
            if change.parent_id == (None, None) or change.kind == (None, None):
1051
 
                continue
1052
 
            if change.kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
1053
 
                warning(
1054
 
                    'Ignoring "%s" as symlinks are not '
1055
 
                    'supported on this filesystem.' % (change.path[0],))
1056
 
                continue
1057
 
            oldpath, newpath = change.path
1058
 
            oldpath_encoded = get_encoded_path(change.path[0])
1059
 
            newpath_encoded = get_encoded_path(change.path[1])
1060
 
            old_present = (change.kind[0] is not None and change.versioned[0])
1061
 
            new_present = (change.kind[1] is not None and change.versioned[1])
1062
 
            executable = change.executable
1063
 
            kind = change.kind
1064
 
            renamed = (change.parent_id[0], change.name[0]) != (change.parent_id[1], change.name[1])
 
980
            if parent == (None, None) or kind == (None, None):
 
981
                continue
 
982
            oldpath, newpath = paths
 
983
            oldpath_encoded = get_encoded_path(paths[0])
 
984
            newpath_encoded = get_encoded_path(paths[1])
 
985
            old_present = (kind[0] is not None and versioned[0])
 
986
            new_present = (kind[1] is not None and versioned[1])
 
987
            renamed = (parent[0], name[0]) != (parent[1], name[1])
1065
988
 
1066
989
            properties_changed = []
1067
 
            properties_changed.extend(
1068
 
                get_executable_change(executable[0], executable[1]))
 
990
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
1069
991
 
1070
992
            if properties_changed:
1071
993
                prop_str = b" (properties changed: %s)" % (
1072
 
                    b", ".join(properties_changed),)
 
994
                        b", ".join(properties_changed),)
1073
995
            else:
1074
996
                prop_str = b""
1075
997
 
1083
1005
                oldpath = newpath
1084
1006
            elif renamed:
1085
1007
                self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1086
 
                                   (kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
 
1008
                    (kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
1087
1009
            else:
1088
1010
                # if it was produced by iter_changes, it must be
1089
1011
                # modified *somehow*, either content or execute bit.
1090
1012
                self.to_file.write(b"=== modified %s '%s'%s\n" % (kind[0].encode('ascii'),
1091
 
                                                                  newpath_encoded, prop_str))
1092
 
            if change.changed_content:
1093
 
                self._diff(oldpath, newpath, kind[0], kind[1])
 
1013
                                   newpath_encoded, prop_str))
 
1014
            if changed_content:
 
1015
                self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
1094
1016
                has_changes = 1
1095
1017
            if renamed:
1096
1018
                has_changes = 1
1097
1019
        return has_changes
1098
1020
 
1099
 
    def diff(self, old_path, new_path):
 
1021
    def diff(self, file_id, old_path, new_path):
1100
1022
        """Perform a diff of a single file
1101
1023
 
 
1024
        :param file_id: file-id of the file
1102
1025
        :param old_path: The path of the file in the old tree
1103
1026
        :param new_path: The path of the file in the new tree
1104
1027
        """
1105
1028
        if old_path is None:
1106
1029
            old_kind = None
1107
1030
        else:
1108
 
            old_kind = self.old_tree.kind(old_path)
 
1031
            old_kind = self.old_tree.kind(old_path, file_id)
1109
1032
        if new_path is None:
1110
1033
            new_kind = None
1111
1034
        else:
1112
 
            new_kind = self.new_tree.kind(new_path)
1113
 
        self._diff(old_path, new_path, old_kind, new_kind)
 
1035
            new_kind = self.new_tree.kind(new_path, file_id)
 
1036
        self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1114
1037
 
1115
 
    def _diff(self, old_path, new_path, old_kind, new_kind):
1116
 
        result = DiffPath._diff_many(
1117
 
            self.differs, old_path, new_path, old_kind, new_kind)
 
1038
    def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
 
1039
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
1040
                                       new_path, old_kind, new_kind)
1118
1041
        if result is DiffPath.CANNOT_DIFF:
1119
1042
            error_path = new_path
1120
1043
            if error_path is None: