/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-02-18 21:42:57 UTC
  • mto: This revision was merged to the branch mainline in revision 6859.
  • Revision ID: jelmer@jelmer.uk-20180218214257-jpevutp1wa30tz3v
Update TODO to reference Breezy, not Bazaar.

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