/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

  • Committer: Jelmer Vernooij
  • Date: 2018-05-06 11:48:54 UTC
  • mto: This revision was merged to the branch mainline in revision 6960.
  • Revision ID: jelmer@jelmer.uk-20180506114854-h4qd9ojaqy8wxjsd
Move .mailmap to root.

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.
82
86
    # In the meantime we at least make sure the patch isn't
83
87
    # mangled.
84
88
 
 
89
 
 
90
    # Special workaround for Python2.3, where difflib fails if
 
91
    # both sequences are empty.
 
92
    if not oldlines and not newlines:
 
93
        return
 
94
 
85
95
    if allow_binary is False:
86
96
        textfile.check_text_lines(oldlines)
87
97
        textfile.check_text_lines(newlines)
88
98
 
89
99
    if sequence_matcher is None:
90
100
        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)
 
101
    ud = patiencediff.unified_diff(oldlines, newlines,
 
102
                      fromfile=old_filename.encode(path_encoding, 'replace'),
 
103
                      tofile=new_filename.encode(path_encoding, 'replace'),
 
104
                      n=context_lines, sequencematcher=sequence_matcher)
96
105
 
97
106
    ud = list(ud)
98
 
    if len(ud) == 0:  # Identical contents, nothing to do
 
107
    if len(ud) == 0: # Identical contents, nothing to do
99
108
        return
100
109
    # work-around for difflib being too smart for its own good
101
110
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
102
111
    if not oldlines:
103
 
        ud[2] = ud[2].replace(b'-1,0', b'-0,0')
 
112
        ud[2] = ud[2].replace('-1,0', '-0,0')
104
113
    elif not newlines:
105
 
        ud[2] = ud[2].replace(b'+1,0', b'+0,0')
 
114
        ud[2] = ud[2].replace('+1,0', '+0,0')
106
115
 
107
116
    for line in ud:
108
117
        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
 
118
        if not line.endswith('\n'):
 
119
            to_file.write("\n\\ No newline at end of file\n")
 
120
    to_file.write('\n')
181
121
 
182
122
 
183
123
def _spawn_external_diff(diffcmd, capture_errors=True):
216
156
 
217
157
    return pipe
218
158
 
219
 
 
220
159
# diff style options as of GNU diff v3.2
221
160
style_option_list = ['-c', '-C', '--context',
222
161
                     '-e', '--ed',
228
167
                     '-y', '--side-by-side',
229
168
                     '-D', '--ifdef']
230
169
 
231
 
 
232
170
def default_style_unified(diff_opts):
233
171
    """Default to unified diff style if alternative not specified in diff_opts.
234
172
 
253
191
    return diff_opts
254
192
 
255
193
 
256
 
def external_diff(old_label, oldlines, new_label, newlines, to_file,
 
194
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
257
195
                  diff_opts):
258
196
    """Display a diff by calling out to the external diff program."""
259
197
    # make sure our own output is properly ordered before the diff
283
221
        if sys.platform == 'win32':
284
222
            # Popen doesn't do the proper encoding for external commands
285
223
            # 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')
 
224
            old_filename = old_filename.encode('mbcs')
 
225
            new_filename = new_filename.encode('mbcs')
288
226
        diffcmd = ['diff',
289
 
                   '--label', old_label,
 
227
                   '--label', old_filename,
290
228
                   old_abspath,
291
 
                   '--label', new_label,
 
229
                   '--label', new_filename,
292
230
                   new_abspath,
293
231
                   '--binary',
294
 
                   ]
 
232
                  ]
295
233
 
296
234
        diff_opts = default_style_unified(diff_opts)
297
235
 
303
241
        rc = pipe.returncode
304
242
 
305
243
        # internal_diff() adds a trailing newline, add one here for consistency
306
 
        out += b'\n'
 
244
        out += '\n'
307
245
        if rc == 2:
308
246
            # 'diff' gives retcode == 2 for all sorts of errors
309
247
            # one of those is 'Binary files differ'.
316
254
            out, err = pipe.communicate()
317
255
 
318
256
            # Write out the new i18n diff response
319
 
            to_file.write(out + b'\n')
 
257
            to_file.write(out+'\n')
320
258
            if pipe.returncode != 2:
321
259
                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,))
 
260
                               'external diff failed with exit code 2'
 
261
                               ' when run with LANG=C and LC_ALL=C,'
 
262
                               ' but not when run natively: %r' % (diffcmd,))
325
263
 
326
 
            first_line = lang_c_out.split(b'\n', 1)[0]
 
264
            first_line = lang_c_out.split('\n', 1)[0]
327
265
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
328
 
            m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
 
266
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
329
267
            if m is None:
330
268
                raise errors.BzrError('external diff failed with exit code 2;'
331
269
                                      ' command: %r' % (diffcmd,))
346
284
            raise errors.BzrError('external diff failed with %s; command: %r'
347
285
                                  % (msg, diffcmd))
348
286
 
 
287
 
349
288
    finally:
350
289
        oldtmpf.close()                 # and delete
351
290
        newtmpf.close()
365
304
 
366
305
 
367
306
def get_trees_and_branches_to_diff_locked(
368
 
        path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
 
307
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
369
308
    """Get the trees and specific files to diff given a list of paths.
370
309
 
371
310
    This method works out the trees to be diff'ed and the files of
382
321
    :param new_url:
383
322
        The url of the new branch or tree. If None, the tree to use is
384
323
        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
 
324
    :param add_cleanup:
 
325
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
387
326
        will register cleanups that must be run to unlock the trees, etc.
388
327
    :param apply_view:
389
328
        if True and a view is set, apply the view or check that the paths
392
331
        a tuple of (old_tree, new_tree, old_branch, new_branch,
393
332
        specific_files, extra_trees) where extra_trees is a sequence of
394
333
        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
 
334
        will be read-locked until the cleanups registered via the add_cleanup
396
335
        param are run.
397
336
    """
398
337
    # Get the old and new revision specs
424
363
 
425
364
    def lock_tree_or_branch(wt, br):
426
365
        if wt is not None:
427
 
            exit_stack.enter_context(wt.lock_read())
 
366
            wt.lock_read()
 
367
            add_cleanup(wt.unlock)
428
368
        elif br is not None:
429
 
            exit_stack.enter_context(br.lock_read())
 
369
            br.lock_read()
 
370
            add_cleanup(br.unlock)
430
371
 
431
372
    # Get the old location
432
373
    specific_files = []
454
395
                views.check_path_in_view(working_tree, relpath)
455
396
            specific_files.append(relpath)
456
397
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
457
 
                                 basis_is_default=working_tree is None)
 
398
        basis_is_default=working_tree is None)
458
399
    new_branch = branch
459
400
 
460
401
    # Get the specific files (all files is None, no files is [])
465
406
    specific_files.extend(other_paths)
466
407
    if len(specific_files) == 0:
467
408
        specific_files = None
468
 
        if (working_tree is not None and working_tree.supports_views() and
469
 
                apply_view):
 
409
        if (working_tree is not None and working_tree.supports_views()
 
410
            and apply_view):
470
411
            view_files = working_tree.views.lookup_view()
471
412
            if view_files:
472
413
                specific_files = view_files
508
449
    :param to_file: The output stream.
509
450
    :param specific_files: Include only changes to these files - None for all
510
451
        changes.
511
 
    :param external_diff_options: If set, use an external GNU diff and pass
 
452
    :param external_diff_options: If set, use an external GNU diff and pass 
512
453
        these options.
513
454
    :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,
 
455
    :param path_encoding: If set, the path will be encoded as specified, 
515
456
        otherwise is supposed to be utf8
516
457
    :param format_cls: Formatter class (DiffTree subclass)
517
458
    """
519
460
        context = DEFAULT_CONTEXT_AMOUNT
520
461
    if format_cls is None:
521
462
        format_cls = DiffTree
522
 
    with cleanup.ExitStack() as exit_stack:
523
 
        exit_stack.enter_context(old_tree.lock_read())
 
463
    with old_tree.lock_read():
524
464
        if extra_trees is not None:
525
465
            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):
 
466
                tree.lock_read()
 
467
        new_tree.lock_read()
 
468
        try:
 
469
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
470
                                                   path_encoding,
 
471
                                                   external_diff_options,
 
472
                                                   old_label, new_label, using,
 
473
                                                   context_lines=context)
 
474
            return differ.show_diff(specific_files, extra_trees)
 
475
        finally:
 
476
            new_tree.unlock()
 
477
            if extra_trees is not None:
 
478
                for tree in extra_trees:
 
479
                    tree.unlock()
 
480
 
 
481
 
 
482
def _patch_header_date(tree, file_id, path):
537
483
    """Returns a timestamp suitable for use in a patch header."""
538
484
    try:
539
 
        mtime = tree.get_file_mtime(path)
 
485
        mtime = tree.get_file_mtime(path, file_id)
540
486
    except FileTimestampUnavailable:
541
487
        mtime = 0
542
488
    return timestamp.format_patch_date(mtime)
543
489
 
544
490
 
545
491
def get_executable_change(old_is_x, new_is_x):
546
 
    descr = {True: b"+x", False: b"-x", None: b"??"}
 
492
    descr = { True:"+x", False:"-x", None:"??" }
547
493
    if old_is_x != new_is_x:
548
 
        return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
 
494
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
549
495
    else:
550
496
        return []
551
497
 
582
528
                     diff_tree.to_file, diff_tree.path_encoding)
583
529
 
584
530
    @staticmethod
585
 
    def _diff_many(differs, old_path, new_path, old_kind, new_kind):
 
531
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
586
532
        for file_differ in differs:
587
 
            result = file_differ.diff(old_path, new_path, old_kind, new_kind)
 
533
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
 
534
                                      new_kind)
588
535
            if result is not DiffPath.CANNOT_DIFF:
589
536
                return result
590
537
        else:
597
544
    Represents kind change as deletion + creation.  Uses the other differs
598
545
    to do this.
599
546
    """
600
 
 
601
547
    def __init__(self, differs):
602
548
        self.differs = differs
603
549
 
608
554
    def from_diff_tree(klass, diff_tree):
609
555
        return klass(diff_tree.differs)
610
556
 
611
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
557
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
612
558
        """Perform comparison
613
559
 
 
560
        :param file_id: The file_id of the file to compare
614
561
        :param old_path: Path of the file in the old tree
615
562
        :param new_path: Path of the file in the new tree
616
563
        :param old_kind: Old file-kind of the file
618
565
        """
619
566
        if None in (old_kind, new_kind):
620
567
            return DiffPath.CANNOT_DIFF
621
 
        result = DiffPath._diff_many(
622
 
            self.differs, old_path, new_path, old_kind, None)
 
568
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
569
                                       new_path, old_kind, None)
623
570
        if result is DiffPath.CANNOT_DIFF:
624
571
            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
 
572
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
 
573
                                     None, new_kind)
642
574
 
643
575
 
644
576
class DiffDirectory(DiffPath):
645
577
 
646
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
578
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
647
579
        """Perform comparison between two directories.  (dummy)
648
580
 
649
581
        """
658
590
 
659
591
class DiffSymlink(DiffPath):
660
592
 
661
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
593
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
662
594
        """Perform comparison between two symlinks
663
595
 
 
596
        :param file_id: The file_id of the file to compare
664
597
        :param old_path: Path of the file in the old tree
665
598
        :param new_path: Path of the file in the new tree
666
599
        :param old_kind: Old file-kind of the file
669
602
        if 'symlink' not in (old_kind, new_kind):
670
603
            return self.CANNOT_DIFF
671
604
        if old_kind == 'symlink':
672
 
            old_target = self.old_tree.get_symlink_target(old_path)
 
605
            old_target = self.old_tree.get_symlink_target(old_path, file_id)
673
606
        elif old_kind is None:
674
607
            old_target = None
675
608
        else:
676
609
            return self.CANNOT_DIFF
677
610
        if new_kind == 'symlink':
678
 
            new_target = self.new_tree.get_symlink_target(new_path)
 
611
            new_target = self.new_tree.get_symlink_target(new_path, file_id)
679
612
        elif new_kind is None:
680
613
            new_target = None
681
614
        else:
684
617
 
685
618
    def diff_symlink(self, old_target, new_target):
686
619
        if old_target is None:
687
 
            self.to_file.write(b'=== target is \'%s\'\n' %
688
 
                               new_target.encode(self.path_encoding, 'replace'))
 
620
            self.to_file.write('=== target is %r\n' % new_target)
689
621
        elif new_target is None:
690
 
            self.to_file.write(b'=== target was \'%s\'\n' %
691
 
                               old_target.encode(self.path_encoding, 'replace'))
 
622
            self.to_file.write('=== target was %r\n' % old_target)
692
623
        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')))
 
624
            self.to_file.write('=== target changed %r => %r\n' %
 
625
                              (old_target, new_target))
696
626
        return self.CHANGED
697
627
 
698
628
 
702
632
    # or removed in a diff.
703
633
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
704
634
 
705
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
706
 
                 old_label='', new_label='', text_differ=internal_diff,
 
635
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8', 
 
636
                 old_label='', new_label='', text_differ=internal_diff, 
707
637
                 context_lines=DEFAULT_CONTEXT_AMOUNT):
708
638
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
709
639
        self.text_differ = text_differ
712
642
        self.path_encoding = path_encoding
713
643
        self.context_lines = context_lines
714
644
 
715
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
645
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
716
646
        """Compare two files in unified diff format
717
647
 
 
648
        :param file_id: The file_id of the file to compare
718
649
        :param old_path: Path of the file in the old tree
719
650
        :param new_path: Path of the file in the new tree
720
651
        :param old_kind: Old file-kind of the file
722
653
        """
723
654
        if 'file' not in (old_kind, new_kind):
724
655
            return self.CANNOT_DIFF
 
656
        from_file_id = to_file_id = file_id
725
657
        if old_kind == 'file':
726
 
            old_date = _patch_header_date(self.old_tree, old_path)
 
658
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
727
659
        elif old_kind is None:
728
660
            old_date = self.EPOCH_DATE
 
661
            from_file_id = None
729
662
        else:
730
663
            return self.CANNOT_DIFF
731
664
        if new_kind == 'file':
732
 
            new_date = _patch_header_date(self.new_tree, new_path)
 
665
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
733
666
        elif new_kind is None:
734
667
            new_date = self.EPOCH_DATE
 
668
            to_file_id = None
735
669
        else:
736
670
            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)
 
671
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
 
672
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
 
673
        return self.diff_text(old_path, new_path, from_label, to_label,
 
674
            from_file_id, to_file_id)
742
675
 
743
 
    def diff_text(self, from_path, to_path, from_label, to_label):
 
676
    def diff_text(self, from_path, to_path, from_label, to_label,
 
677
        from_file_id=None, to_file_id=None):
744
678
        """Diff the content of given files in two trees
745
679
 
746
680
        :param from_path: The path in the from tree. If None,
748
682
        :param to_path: The path in the to tree. This may refer
749
683
            to a different file from from_path.  If None,
750
684
            the file is not present in the to tree.
 
685
        :param from_file_id: The id of the file in the from tree or None if
 
686
            unknown.
 
687
        :param to_file_id: The id of the file in the to tree or None if
 
688
            unknown.
751
689
        """
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 []
 
690
        def _get_text(tree, file_id, path):
 
691
            if file_id is None:
 
692
                return []
 
693
            return tree.get_file_lines(path, file_id)
759
694
        try:
760
 
            from_text = _get_text(self.old_tree, from_path)
761
 
            to_text = _get_text(self.new_tree, to_path)
 
695
            from_text = _get_text(self.old_tree, from_file_id, from_path)
 
696
            to_text = _get_text(self.new_tree, to_file_id, to_path)
762
697
            self.text_differ(from_label, from_text, to_label, to_text,
763
698
                             self.to_file, path_encoding=self.path_encoding,
764
699
                             context_lines=self.context_lines)
765
700
        except errors.BinaryFile:
766
701
            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'))
 
702
                  ("Binary files %s and %s differ\n" %
 
703
                  (from_label, to_label)).encode(self.path_encoding, 'replace'))
769
704
        return self.CHANGED
770
705
 
771
706
 
778
713
        self._root = osutils.mkdtemp(prefix='brz-diff-')
779
714
 
780
715
    @classmethod
781
 
    def from_string(klass, command_template, old_tree, new_tree, to_file,
 
716
    def from_string(klass, command_string, old_tree, new_tree, to_file,
782
717
                    path_encoding='utf-8'):
 
718
        command_template = cmdline.split(command_string)
 
719
        if '@' not in command_string:
 
720
            command_template.extend(['@old_path', '@new_path'])
783
721
        return klass(command_template, old_tree, new_tree, to_file,
784
722
                     path_encoding)
785
723
 
795
733
 
796
734
    def _get_command(self, old_path, new_path):
797
735
        my_map = {'old_path': old_path, 'new_path': new_path}
798
 
        command = [t.format(**my_map) for t in
 
736
        command = [AtTemplate(t).substitute(my_map) for t in
799
737
                   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
 
738
        if sys.platform == 'win32': # Popen doesn't accept unicode on win32
803
739
            command_encoded = []
804
740
            for c in command:
805
 
                if isinstance(c, text_type):
 
741
                if isinstance(c, unicode):
806
742
                    command_encoded.append(c.encode('mbcs'))
807
743
                else:
808
744
                    command_encoded.append(c)
821
757
            else:
822
758
                raise
823
759
        self.to_file.write(proc.stdout.read())
824
 
        proc.stdout.close()
825
760
        return proc.wait()
826
761
 
827
762
    def _try_symlink_root(self, tree, prefix):
828
 
        if (getattr(tree, 'abspath', None) is None or
829
 
                not osutils.host_os_dereferences_symlinks()):
 
763
        if (getattr(tree, 'abspath', None) is None
 
764
            or not osutils.host_os_dereferences_symlinks()):
830
765
            return False
831
766
        try:
832
767
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
864
799
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
865
800
 
866
801
    def _write_file(self, relpath, tree, prefix, force_temp=False,
867
 
                    allow_write=False):
 
802
                    allow_write=False, file_id=None):
868
803
        if not force_temp and isinstance(tree, WorkingTree):
869
804
            full_path = tree.abspath(relpath)
870
805
            if self._is_safepath(full_path):
879
814
        except OSError as e:
880
815
            if e.errno != errno.EEXIST:
881
816
                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)
 
817
        source = tree.get_file(relpath, file_id)
 
818
        try:
 
819
            with open(full_path, 'wb') as target:
 
820
                osutils.pumpfile(source, target)
 
821
        finally:
 
822
            source.close()
 
823
        try:
 
824
            mtime = tree.get_file_mtime(relpath, file_id)
887
825
        except FileTimestampUnavailable:
888
826
            pass
889
827
        else:
893
831
        return full_path
894
832
 
895
833
    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)
 
834
                       allow_write_new=False, file_id=None):
 
835
        old_disk_path = self._write_file(old_path, self.old_tree, 'old',
 
836
                                         force_temp, file_id=file_id)
 
837
        new_disk_path = self._write_file(new_path, self.new_tree, 'new',
 
838
                                         force_temp, file_id=file_id,
 
839
                                         allow_write=allow_write_new)
902
840
        return old_disk_path, new_disk_path
903
841
 
904
842
    def finish(self):
907
845
        except OSError as e:
908
846
            if e.errno != errno.ENOENT:
909
847
                mutter("The temporary directory \"%s\" was not "
910
 
                       "cleanly removed: %s." % (self._root, e))
 
848
                        "cleanly removed: %s." % (self._root, e))
911
849
 
912
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
850
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
913
851
        if (old_kind, new_kind) != ('file', 'file'):
914
852
            return DiffPath.CANNOT_DIFF
915
853
        (old_disk_path, new_disk_path) = self._prepare_files(
916
 
            old_path, new_path)
 
854
                old_path, new_path, file_id=file_id)
917
855
        self._execute(old_disk_path, new_disk_path)
918
856
 
919
 
    def edit_file(self, old_path, new_path):
 
857
    def edit_file(self, old_path, new_path, file_id=None):
920
858
        """Use this tool to edit a file.
921
859
 
922
860
        A temporary copy will be edited, and the new contents will be
923
861
        returned.
924
862
 
 
863
        :param file_id: The id of the file to edit.
925
864
        :return: The new contents of the file.
926
865
        """
927
866
        old_abs_path, new_abs_path = self._prepare_files(
928
 
            old_path, new_path, allow_write_new=True, force_temp=True)
 
867
                old_path, new_path, allow_write_new=True, force_temp=True,
 
868
                file_id=file_id)
929
869
        command = self._get_command(old_abs_path, new_abs_path)
930
870
        subprocess.call(command, cwd=self._root)
931
871
        with open(new_abs_path, 'rb') as new_file:
947
887
    # list of factories that can provide instances of DiffPath objects
948
888
    # may be extended by plugins.
949
889
    diff_factories = [DiffSymlink.from_diff_tree,
950
 
                      DiffDirectory.from_diff_tree,
951
 
                      DiffTreeReference.from_diff_tree]
 
890
                      DiffDirectory.from_diff_tree]
952
891
 
953
892
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
954
893
                 diff_text=None, extra_factories=None):
964
903
            DiffPaths"""
965
904
        if diff_text is None:
966
905
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
967
 
                                 '', '', internal_diff)
 
906
                                 '', '',  internal_diff)
968
907
        self.old_tree = old_tree
969
908
        self.new_tree = new_tree
970
909
        self.to_file = to_file
994
933
        :param using: Commandline to use to invoke an external diff tool
995
934
        """
996
935
        if using is not None:
997
 
            extra_factories = [DiffFromTool.make_from_diff_tree(
998
 
                using, external_diff_options)]
 
936
            extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
999
937
        else:
1000
938
            extra_factories = []
1001
939
        if external_diff_options:
1002
940
            opts = external_diff_options.split()
1003
 
 
1004
941
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
1005
942
                """:param path_encoding: not used but required
1006
943
                        to match the signature of internal_diff.
1029
966
        # TODO: Generation of pseudo-diffs for added/deleted files could
1030
967
        # be usefully made into a much faster special case.
1031
968
        iterator = self.new_tree.iter_changes(self.old_tree,
1032
 
                                              specific_files=specific_files,
1033
 
                                              extra_trees=extra_trees,
1034
 
                                              require_versioned=True)
 
969
                                               specific_files=specific_files,
 
970
                                               extra_trees=extra_trees,
 
971
                                               require_versioned=True)
1035
972
        has_changes = 0
1036
 
 
1037
973
        def changes_key(change):
1038
 
            old_path, new_path = change.path
 
974
            old_path, new_path = change[1]
1039
975
            path = new_path
1040
976
            if path is None:
1041
977
                path = old_path
1042
978
            return path
1043
 
 
1044
979
        def get_encoded_path(path):
1045
980
            if path is not None:
1046
981
                return path.encode(self.path_encoding, "replace")
1047
 
        for change in sorted(iterator, key=changes_key):
 
982
        for (file_id, paths, changed_content, versioned, parent, name, kind,
 
983
             executable) in sorted(iterator, key=changes_key):
1048
984
            # The root does not get diffed, and items with no known kind (that
1049
985
            # 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])
 
986
            if parent == (None, None) or kind == (None, None):
 
987
                continue
 
988
            oldpath, newpath = paths
 
989
            oldpath_encoded = get_encoded_path(paths[0])
 
990
            newpath_encoded = get_encoded_path(paths[1])
 
991
            old_present = (kind[0] is not None and versioned[0])
 
992
            new_present = (kind[1] is not None and versioned[1])
 
993
            renamed = (parent[0], name[0]) != (parent[1], name[1])
1065
994
 
1066
995
            properties_changed = []
1067
 
            properties_changed.extend(
1068
 
                get_executable_change(executable[0], executable[1]))
 
996
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
1069
997
 
1070
998
            if properties_changed:
1071
 
                prop_str = b" (properties changed: %s)" % (
1072
 
                    b", ".join(properties_changed),)
 
999
                prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1073
1000
            else:
1074
 
                prop_str = b""
 
1001
                prop_str = ""
1075
1002
 
1076
1003
            if (old_present, new_present) == (True, False):
1077
 
                self.to_file.write(b"=== removed %s '%s'\n" %
1078
 
                                   (kind[0].encode('ascii'), oldpath_encoded))
 
1004
                self.to_file.write("=== removed %s '%s'\n" %
 
1005
                                   (kind[0], oldpath_encoded))
1079
1006
                newpath = oldpath
1080
1007
            elif (old_present, new_present) == (False, True):
1081
 
                self.to_file.write(b"=== added %s '%s'\n" %
1082
 
                                   (kind[1].encode('ascii'), newpath_encoded))
 
1008
                self.to_file.write("=== added %s '%s'\n" %
 
1009
                                   (kind[1], newpath_encoded))
1083
1010
                oldpath = newpath
1084
1011
            elif renamed:
1085
 
                self.to_file.write(b"=== renamed %s '%s' => '%s'%s\n" %
1086
 
                                   (kind[0].encode('ascii'), oldpath_encoded, newpath_encoded, prop_str))
 
1012
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
 
1013
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
1087
1014
            else:
1088
1015
                # if it was produced by iter_changes, it must be
1089
1016
                # modified *somehow*, either content or execute bit.
1090
 
                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])
 
1017
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
 
1018
                                   newpath_encoded, prop_str))
 
1019
            if changed_content:
 
1020
                self._diff(oldpath, newpath, kind[0], kind[1], file_id=file_id)
1094
1021
                has_changes = 1
1095
1022
            if renamed:
1096
1023
                has_changes = 1
1097
1024
        return has_changes
1098
1025
 
1099
 
    def diff(self, old_path, new_path):
 
1026
    def diff(self, file_id, old_path, new_path):
1100
1027
        """Perform a diff of a single file
1101
1028
 
 
1029
        :param file_id: file-id of the file
1102
1030
        :param old_path: The path of the file in the old tree
1103
1031
        :param new_path: The path of the file in the new tree
1104
1032
        """
1105
1033
        if old_path is None:
1106
1034
            old_kind = None
1107
1035
        else:
1108
 
            old_kind = self.old_tree.kind(old_path)
 
1036
            old_kind = self.old_tree.kind(old_path, file_id)
1109
1037
        if new_path is None:
1110
1038
            new_kind = None
1111
1039
        else:
1112
 
            new_kind = self.new_tree.kind(new_path)
1113
 
        self._diff(old_path, new_path, old_kind, new_kind)
 
1040
            new_kind = self.new_tree.kind(new_path, file_id)
 
1041
        self._diff(old_path, new_path, old_kind, new_kind, file_id=file_id)
1114
1042
 
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)
 
1043
    def _diff(self, old_path, new_path, old_kind, new_kind, file_id):
 
1044
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
1045
                                       new_path, old_kind, new_kind)
1118
1046
        if result is DiffPath.CANNOT_DIFF:
1119
1047
            error_path = new_path
1120
1048
            if error_path is None: