/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: 2017-06-08 23:30:31 UTC
  • mto: This revision was merged to the branch mainline in revision 6690.
  • Revision ID: jelmer@jelmer.uk-20170608233031-3qavls2o7a1pqllj
Update imports.

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
    )
48
50
from .trace import mutter, note, warning
49
 
from .tree import FileTimestampUnavailable
50
 
 
51
51
 
52
52
DEFAULT_CONTEXT_AMOUNT = 3
53
53
 
 
54
class AtTemplate(string.Template):
 
55
    """Templating class that uses @ instead of $."""
 
56
 
 
57
    delimiter = '@'
 
58
 
54
59
 
55
60
# TODO: Rather than building a changeset object, we should probably
56
61
# invoke callbacks on an object.  That object can either accumulate a
66
71
        self.opcodes = None
67
72
 
68
73
 
69
 
def internal_diff(old_label, oldlines, new_label, newlines, to_file,
 
74
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
70
75
                  allow_binary=False, sequence_matcher=None,
71
76
                  path_encoding='utf8', context_lines=DEFAULT_CONTEXT_AMOUNT):
72
77
    # FIXME: difflib is wrong if there is no trailing newline.
79
84
    # In the meantime we at least make sure the patch isn't
80
85
    # mangled.
81
86
 
 
87
 
 
88
    # Special workaround for Python2.3, where difflib fails if
 
89
    # both sequences are empty.
 
90
    if not oldlines and not newlines:
 
91
        return
 
92
 
82
93
    if allow_binary is False:
83
94
        textfile.check_text_lines(oldlines)
84
95
        textfile.check_text_lines(newlines)
85
96
 
86
97
    if sequence_matcher is None:
87
98
        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)
 
99
    ud = patiencediff.unified_diff(oldlines, newlines,
 
100
                      fromfile=old_filename.encode(path_encoding, 'replace'),
 
101
                      tofile=new_filename.encode(path_encoding, 'replace'),
 
102
                      n=context_lines, sequencematcher=sequence_matcher)
93
103
 
94
104
    ud = list(ud)
95
 
    if len(ud) == 0:  # Identical contents, nothing to do
 
105
    if len(ud) == 0: # Identical contents, nothing to do
96
106
        return
97
107
    # work-around for difflib being too smart for its own good
98
108
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
99
109
    if not oldlines:
100
 
        ud[2] = ud[2].replace(b'-1,0', b'-0,0')
 
110
        ud[2] = ud[2].replace('-1,0', '-0,0')
101
111
    elif not newlines:
102
 
        ud[2] = ud[2].replace(b'+1,0', b'+0,0')
 
112
        ud[2] = ud[2].replace('+1,0', '+0,0')
103
113
 
104
114
    for line in ud:
105
115
        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
 
116
        if not line.endswith('\n'):
 
117
            to_file.write("\n\\ No newline at end of file\n")
 
118
    to_file.write('\n')
178
119
 
179
120
 
180
121
def _spawn_external_diff(diffcmd, capture_errors=True):
213
154
 
214
155
    return pipe
215
156
 
216
 
 
217
157
# diff style options as of GNU diff v3.2
218
158
style_option_list = ['-c', '-C', '--context',
219
159
                     '-e', '--ed',
225
165
                     '-y', '--side-by-side',
226
166
                     '-D', '--ifdef']
227
167
 
228
 
 
229
168
def default_style_unified(diff_opts):
230
169
    """Default to unified diff style if alternative not specified in diff_opts.
231
170
 
250
189
    return diff_opts
251
190
 
252
191
 
253
 
def external_diff(old_label, oldlines, new_label, newlines, to_file,
 
192
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
254
193
                  diff_opts):
255
194
    """Display a diff by calling out to the external diff program."""
256
195
    # make sure our own output is properly ordered before the diff
257
196
    to_file.flush()
258
197
 
259
 
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='brz-diff-old-')
260
 
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='brz-diff-new-')
 
198
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
 
199
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
261
200
    oldtmpf = os.fdopen(oldtmp_fd, 'wb')
262
201
    newtmpf = os.fdopen(newtmp_fd, 'wb')
263
202
 
280
219
        if sys.platform == 'win32':
281
220
            # Popen doesn't do the proper encoding for external commands
282
221
            # 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')
 
222
            old_filename = old_filename.encode('mbcs')
 
223
            new_filename = new_filename.encode('mbcs')
285
224
        diffcmd = ['diff',
286
 
                   '--label', old_label,
 
225
                   '--label', old_filename,
287
226
                   old_abspath,
288
 
                   '--label', new_label,
 
227
                   '--label', new_filename,
289
228
                   new_abspath,
290
229
                   '--binary',
291
 
                   ]
 
230
                  ]
292
231
 
293
232
        diff_opts = default_style_unified(diff_opts)
294
233
 
296
235
            diffcmd.extend(diff_opts)
297
236
 
298
237
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
299
 
        out, err = pipe.communicate()
 
238
        out,err = pipe.communicate()
300
239
        rc = pipe.returncode
301
240
 
302
241
        # internal_diff() adds a trailing newline, add one here for consistency
303
 
        out += b'\n'
 
242
        out += '\n'
304
243
        if rc == 2:
305
244
            # 'diff' gives retcode == 2 for all sorts of errors
306
245
            # one of those is 'Binary files differ'.
313
252
            out, err = pipe.communicate()
314
253
 
315
254
            # Write out the new i18n diff response
316
 
            to_file.write(out + b'\n')
 
255
            to_file.write(out+'\n')
317
256
            if pipe.returncode != 2:
318
257
                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,))
 
258
                               'external diff failed with exit code 2'
 
259
                               ' when run with LANG=C and LC_ALL=C,'
 
260
                               ' but not when run natively: %r' % (diffcmd,))
322
261
 
323
 
            first_line = lang_c_out.split(b'\n', 1)[0]
 
262
            first_line = lang_c_out.split('\n', 1)[0]
324
263
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
325
 
            m = re.match(b'^(binary )?files.*differ$', first_line, re.I)
 
264
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
326
265
            if m is None:
327
266
                raise errors.BzrError('external diff failed with exit code 2;'
328
267
                                      ' command: %r' % (diffcmd,))
343
282
            raise errors.BzrError('external diff failed with %s; command: %r'
344
283
                                  % (msg, diffcmd))
345
284
 
 
285
 
346
286
    finally:
347
287
        oldtmpf.close()                 # and delete
348
288
        newtmpf.close()
362
302
 
363
303
 
364
304
def get_trees_and_branches_to_diff_locked(
365
 
        path_list, revision_specs, old_url, new_url, exit_stack, apply_view=True):
 
305
    path_list, revision_specs, old_url, new_url, add_cleanup, apply_view=True):
366
306
    """Get the trees and specific files to diff given a list of paths.
367
307
 
368
308
    This method works out the trees to be diff'ed and the files of
379
319
    :param new_url:
380
320
        The url of the new branch or tree. If None, the tree to use is
381
321
        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
 
322
    :param add_cleanup:
 
323
        a callable like Command.add_cleanup.  get_trees_and_branches_to_diff
384
324
        will register cleanups that must be run to unlock the trees, etc.
385
325
    :param apply_view:
386
326
        if True and a view is set, apply the view or check that the paths
389
329
        a tuple of (old_tree, new_tree, old_branch, new_branch,
390
330
        specific_files, extra_trees) where extra_trees is a sequence of
391
331
        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
 
332
        will be read-locked until the cleanups registered via the add_cleanup
393
333
        param are run.
394
334
    """
395
335
    # Get the old and new revision specs
421
361
 
422
362
    def lock_tree_or_branch(wt, br):
423
363
        if wt is not None:
424
 
            exit_stack.enter_context(wt.lock_read())
 
364
            wt.lock_read()
 
365
            add_cleanup(wt.unlock)
425
366
        elif br is not None:
426
 
            exit_stack.enter_context(br.lock_read())
 
367
            br.lock_read()
 
368
            add_cleanup(br.unlock)
427
369
 
428
370
    # Get the old location
429
371
    specific_files = []
451
393
                views.check_path_in_view(working_tree, relpath)
452
394
            specific_files.append(relpath)
453
395
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
454
 
                                 basis_is_default=working_tree is None)
 
396
        basis_is_default=working_tree is None)
455
397
    new_branch = branch
456
398
 
457
399
    # Get the specific files (all files is None, no files is [])
462
404
    specific_files.extend(other_paths)
463
405
    if len(specific_files) == 0:
464
406
        specific_files = None
465
 
        if (working_tree is not None and working_tree.supports_views() and
466
 
                apply_view):
 
407
        if (working_tree is not None and working_tree.supports_views()
 
408
            and apply_view):
467
409
            view_files = working_tree.views.lookup_view()
468
410
            if view_files:
469
411
                specific_files = view_files
505
447
    :param to_file: The output stream.
506
448
    :param specific_files: Include only changes to these files - None for all
507
449
        changes.
508
 
    :param external_diff_options: If set, use an external GNU diff and pass
 
450
    :param external_diff_options: If set, use an external GNU diff and pass 
509
451
        these options.
510
452
    :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,
 
453
    :param path_encoding: If set, the path will be encoded as specified, 
512
454
        otherwise is supposed to be utf8
513
455
    :param format_cls: Formatter class (DiffTree subclass)
514
456
    """
516
458
        context = DEFAULT_CONTEXT_AMOUNT
517
459
    if format_cls is None:
518
460
        format_cls = DiffTree
519
 
    with contextlib.ExitStack() as exit_stack:
520
 
        exit_stack.enter_context(old_tree.lock_read())
 
461
    old_tree.lock_read()
 
462
    try:
521
463
        if extra_trees is not None:
522
464
            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):
 
465
                tree.lock_read()
 
466
        new_tree.lock_read()
 
467
        try:
 
468
            differ = format_cls.from_trees_options(old_tree, new_tree, to_file,
 
469
                                                   path_encoding,
 
470
                                                   external_diff_options,
 
471
                                                   old_label, new_label, using,
 
472
                                                   context_lines=context)
 
473
            return differ.show_diff(specific_files, extra_trees)
 
474
        finally:
 
475
            new_tree.unlock()
 
476
            if extra_trees is not None:
 
477
                for tree in extra_trees:
 
478
                    tree.unlock()
 
479
    finally:
 
480
        old_tree.unlock()
 
481
 
 
482
 
 
483
def _patch_header_date(tree, file_id, path):
534
484
    """Returns a timestamp suitable for use in a patch header."""
535
485
    try:
536
 
        mtime = tree.get_file_mtime(path)
537
 
    except FileTimestampUnavailable:
 
486
        mtime = tree.get_file_mtime(file_id, path)
 
487
    except errors.FileTimestampUnavailable:
538
488
        mtime = 0
539
489
    return timestamp.format_patch_date(mtime)
540
490
 
541
491
 
542
492
def get_executable_change(old_is_x, new_is_x):
543
 
    descr = {True: b"+x", False: b"-x", None: b"??"}
 
493
    descr = { True:"+x", False:"-x", None:"??" }
544
494
    if old_is_x != new_is_x:
545
 
        return [b"%s to %s" % (descr[old_is_x], descr[new_is_x],)]
 
495
        return ["%s to %s" % (descr[old_is_x], descr[new_is_x],)]
546
496
    else:
547
497
        return []
548
498
 
579
529
                     diff_tree.to_file, diff_tree.path_encoding)
580
530
 
581
531
    @staticmethod
582
 
    def _diff_many(differs, old_path, new_path, old_kind, new_kind):
 
532
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
583
533
        for file_differ in differs:
584
 
            result = file_differ.diff(old_path, new_path, old_kind, new_kind)
 
534
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
 
535
                                      new_kind)
585
536
            if result is not DiffPath.CANNOT_DIFF:
586
537
                return result
587
538
        else:
594
545
    Represents kind change as deletion + creation.  Uses the other differs
595
546
    to do this.
596
547
    """
597
 
 
598
548
    def __init__(self, differs):
599
549
        self.differs = differs
600
550
 
605
555
    def from_diff_tree(klass, diff_tree):
606
556
        return klass(diff_tree.differs)
607
557
 
608
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
558
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
609
559
        """Perform comparison
610
560
 
 
561
        :param file_id: The file_id of the file to compare
611
562
        :param old_path: Path of the file in the old tree
612
563
        :param new_path: Path of the file in the new tree
613
564
        :param old_kind: Old file-kind of the file
615
566
        """
616
567
        if None in (old_kind, new_kind):
617
568
            return DiffPath.CANNOT_DIFF
618
 
        result = DiffPath._diff_many(
619
 
            self.differs, old_path, new_path, old_kind, None)
 
569
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
570
                                       new_path, old_kind, None)
620
571
        if result is DiffPath.CANNOT_DIFF:
621
572
            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
 
573
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
 
574
                                     None, new_kind)
639
575
 
640
576
 
641
577
class DiffDirectory(DiffPath):
642
578
 
643
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
579
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
644
580
        """Perform comparison between two directories.  (dummy)
645
581
 
646
582
        """
655
591
 
656
592
class DiffSymlink(DiffPath):
657
593
 
658
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
594
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
659
595
        """Perform comparison between two symlinks
660
596
 
 
597
        :param file_id: The file_id of the file to compare
661
598
        :param old_path: Path of the file in the old tree
662
599
        :param new_path: Path of the file in the new tree
663
600
        :param old_kind: Old file-kind of the file
666
603
        if 'symlink' not in (old_kind, new_kind):
667
604
            return self.CANNOT_DIFF
668
605
        if old_kind == 'symlink':
669
 
            old_target = self.old_tree.get_symlink_target(old_path)
 
606
            old_target = self.old_tree.get_symlink_target(file_id)
670
607
        elif old_kind is None:
671
608
            old_target = None
672
609
        else:
673
610
            return self.CANNOT_DIFF
674
611
        if new_kind == 'symlink':
675
 
            new_target = self.new_tree.get_symlink_target(new_path)
 
612
            new_target = self.new_tree.get_symlink_target(file_id)
676
613
        elif new_kind is None:
677
614
            new_target = None
678
615
        else:
681
618
 
682
619
    def diff_symlink(self, old_target, new_target):
683
620
        if old_target is None:
684
 
            self.to_file.write(b'=== target is \'%s\'\n' %
685
 
                               new_target.encode(self.path_encoding, 'replace'))
 
621
            self.to_file.write('=== target is %r\n' % new_target)
686
622
        elif new_target is None:
687
 
            self.to_file.write(b'=== target was \'%s\'\n' %
688
 
                               old_target.encode(self.path_encoding, 'replace'))
 
623
            self.to_file.write('=== target was %r\n' % old_target)
689
624
        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')))
 
625
            self.to_file.write('=== target changed %r => %r\n' %
 
626
                              (old_target, new_target))
693
627
        return self.CHANGED
694
628
 
695
629
 
699
633
    # or removed in a diff.
700
634
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
701
635
 
702
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
703
 
                 old_label='', new_label='', text_differ=internal_diff,
 
636
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8', 
 
637
                 old_label='', new_label='', text_differ=internal_diff, 
704
638
                 context_lines=DEFAULT_CONTEXT_AMOUNT):
705
639
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
706
640
        self.text_differ = text_differ
709
643
        self.path_encoding = path_encoding
710
644
        self.context_lines = context_lines
711
645
 
712
 
    def diff(self, old_path, new_path, old_kind, new_kind):
 
646
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
713
647
        """Compare two files in unified diff format
714
648
 
 
649
        :param file_id: The file_id of the file to compare
715
650
        :param old_path: Path of the file in the old tree
716
651
        :param new_path: Path of the file in the new tree
717
652
        :param old_kind: Old file-kind of the file
719
654
        """
720
655
        if 'file' not in (old_kind, new_kind):
721
656
            return self.CANNOT_DIFF
 
657
        from_file_id = to_file_id = file_id
722
658
        if old_kind == 'file':
723
 
            old_date = _patch_header_date(self.old_tree, old_path)
 
659
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
724
660
        elif old_kind is None:
725
661
            old_date = self.EPOCH_DATE
 
662
            from_file_id = None
726
663
        else:
727
664
            return self.CANNOT_DIFF
728
665
        if new_kind == 'file':
729
 
            new_date = _patch_header_date(self.new_tree, new_path)
 
666
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
730
667
        elif new_kind is None:
731
668
            new_date = self.EPOCH_DATE
 
669
            to_file_id = None
732
670
        else:
733
671
            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)
 
672
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
 
673
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
 
674
        return self.diff_text(from_file_id, to_file_id, from_label, to_label,
 
675
            old_path, new_path)
739
676
 
740
 
    def diff_text(self, from_path, to_path, from_label, to_label):
 
677
    def diff_text(self, from_file_id, to_file_id, from_label, to_label,
 
678
        from_path=None, to_path=None):
741
679
        """Diff the content of given files in two trees
742
680
 
743
 
        :param from_path: The path in the from tree. If None,
 
681
        :param from_file_id: The id of the file in the from tree.  If None,
744
682
            the file is not present in the from tree.
745
 
        :param to_path: The path in the to tree. This may refer
746
 
            to a different file from from_path.  If None,
 
683
        :param to_file_id: The id of the file in the to tree.  This may refer
 
684
            to a different file from from_file_id.  If None,
747
685
            the file is not present in the to tree.
 
686
        :param from_path: The path in the from tree or None if unknown.
 
687
        :param to_path: The path in the to tree or None if unknown.
748
688
        """
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:
 
689
        def _get_text(tree, file_id, path):
 
690
            if file_id is not None:
 
691
                return tree.get_file_lines(file_id, path)
 
692
            else:
755
693
                return []
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
 
774
710
                 path_encoding='utf-8'):
775
711
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
776
712
        self.command_template = command_template
777
 
        self._root = osutils.mkdtemp(prefix='brz-diff-')
 
713
        self._root = osutils.mkdtemp(prefix='bzr-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))
862
798
        relpath_tmp = relpath_tmp.replace(u'?', u'_')
863
799
        return osutils.pathjoin(self._root, prefix, relpath_tmp)
864
800
 
865
 
    def _write_file(self, relpath, tree, prefix, force_temp=False,
 
801
    def _write_file(self, file_id, tree, prefix, relpath, force_temp=False,
866
802
                    allow_write=False):
867
803
        if not force_temp and isinstance(tree, WorkingTree):
868
 
            full_path = tree.abspath(relpath)
 
804
            full_path = tree.abspath(tree.id2path(file_id))
869
805
            if self._is_safepath(full_path):
870
806
                return full_path
871
807
 
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)
886
 
        except FileTimestampUnavailable:
 
817
        source = tree.get_file(file_id, relpath)
 
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(file_id)
 
828
        except errors.FileTimestampUnavailable:
887
829
            pass
888
830
        else:
889
831
            os.utime(full_path, (mtime, mtime))
891
833
            osutils.make_readonly(full_path)
892
834
        return full_path
893
835
 
894
 
    def _prepare_files(self, old_path, new_path, force_temp=False,
 
836
    def _prepare_files(self, file_id, old_path, new_path, force_temp=False,
895
837
                       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)
 
838
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
 
839
                                         old_path, force_temp)
 
840
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
 
841
                                         new_path, force_temp,
 
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
                                                file_id, old_path, new_path)
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, file_id):
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
        """
 
869
        old_path = self.old_tree.id2path(file_id)
 
870
        new_path = self.new_tree.id2path(file_id)
926
871
        old_abs_path, new_abs_path = self._prepare_files(
927
 
            old_path, new_path, allow_write_new=True, force_temp=True)
 
872
                                            file_id, old_path, new_path,
 
873
                                            allow_write_new=True,
 
874
                                            force_temp=True)
928
875
        command = self._get_command(old_abs_path, new_abs_path)
929
876
        subprocess.call(command, cwd=self._root)
930
 
        with open(new_abs_path, 'rb') as new_file:
 
877
        new_file = open(new_abs_path, 'rb')
 
878
        try:
931
879
            return new_file.read()
 
880
        finally:
 
881
            new_file.close()
932
882
 
933
883
 
934
884
class DiffTree(object):
946
896
    # list of factories that can provide instances of DiffPath objects
947
897
    # may be extended by plugins.
948
898
    diff_factories = [DiffSymlink.from_diff_tree,
949
 
                      DiffDirectory.from_diff_tree,
950
 
                      DiffTreeReference.from_diff_tree]
 
899
                      DiffDirectory.from_diff_tree]
951
900
 
952
901
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
953
902
                 diff_text=None, extra_factories=None):
963
912
            DiffPaths"""
964
913
        if diff_text is None:
965
914
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
966
 
                                 '', '', internal_diff)
 
915
                                 '', '',  internal_diff)
967
916
        self.old_tree = old_tree
968
917
        self.new_tree = new_tree
969
918
        self.to_file = to_file
993
942
        :param using: Commandline to use to invoke an external diff tool
994
943
        """
995
944
        if using is not None:
996
 
            extra_factories = [DiffFromTool.make_from_diff_tree(
997
 
                using, external_diff_options)]
 
945
            extra_factories = [DiffFromTool.make_from_diff_tree(using, external_diff_options)]
998
946
        else:
999
947
            extra_factories = []
1000
948
        if external_diff_options:
1001
949
            opts = external_diff_options.split()
1002
 
 
1003
950
            def diff_file(olab, olines, nlab, nlines, to_file, path_encoding=None, context_lines=None):
1004
951
                """:param path_encoding: not used but required
1005
952
                        to match the signature of internal_diff.
1028
975
        # TODO: Generation of pseudo-diffs for added/deleted files could
1029
976
        # be usefully made into a much faster special case.
1030
977
        iterator = self.new_tree.iter_changes(self.old_tree,
1031
 
                                              specific_files=specific_files,
1032
 
                                              extra_trees=extra_trees,
1033
 
                                              require_versioned=True)
 
978
                                               specific_files=specific_files,
 
979
                                               extra_trees=extra_trees,
 
980
                                               require_versioned=True)
1034
981
        has_changes = 0
1035
 
 
1036
982
        def changes_key(change):
1037
 
            old_path, new_path = change.path
 
983
            old_path, new_path = change[1]
1038
984
            path = new_path
1039
985
            if path is None:
1040
986
                path = old_path
1041
987
            return path
1042
 
 
1043
988
        def get_encoded_path(path):
1044
989
            if path is not None:
1045
990
                return path.encode(self.path_encoding, "replace")
1046
 
        for change in sorted(iterator, key=changes_key):
 
991
        for (file_id, paths, changed_content, versioned, parent, name, kind,
 
992
             executable) in sorted(iterator, key=changes_key):
1047
993
            # The root does not get diffed, and items with no known kind (that
1048
994
            # 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])
 
995
            if parent == (None, None) or kind == (None, None):
 
996
                continue
 
997
            oldpath, newpath = paths
 
998
            oldpath_encoded = get_encoded_path(paths[0])
 
999
            newpath_encoded = get_encoded_path(paths[1])
 
1000
            old_present = (kind[0] is not None and versioned[0])
 
1001
            new_present = (kind[1] is not None and versioned[1])
 
1002
            renamed = (parent[0], name[0]) != (parent[1], name[1])
1064
1003
 
1065
1004
            properties_changed = []
1066
 
            properties_changed.extend(
1067
 
                get_executable_change(executable[0], executable[1]))
 
1005
            properties_changed.extend(get_executable_change(executable[0], executable[1]))
1068
1006
 
1069
1007
            if properties_changed:
1070
 
                prop_str = b" (properties changed: %s)" % (
1071
 
                    b", ".join(properties_changed),)
 
1008
                prop_str = " (properties changed: %s)" % (", ".join(properties_changed),)
1072
1009
            else:
1073
 
                prop_str = b""
 
1010
                prop_str = ""
1074
1011
 
1075
1012
            if (old_present, new_present) == (True, False):
1076
 
                self.to_file.write(b"=== removed %s '%s'\n" %
1077
 
                                   (kind[0].encode('ascii'), oldpath_encoded))
 
1013
                self.to_file.write("=== removed %s '%s'\n" %
 
1014
                                   (kind[0], oldpath_encoded))
 
1015
                newpath = oldpath
1078
1016
            elif (old_present, new_present) == (False, True):
1079
 
                self.to_file.write(b"=== added %s '%s'\n" %
1080
 
                                   (kind[1].encode('ascii'), newpath_encoded))
 
1017
                self.to_file.write("=== added %s '%s'\n" %
 
1018
                                   (kind[1], newpath_encoded))
 
1019
                oldpath = newpath
1081
1020
            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))
 
1021
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
 
1022
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
1084
1023
            else:
1085
1024
                # if it was produced by iter_changes, it must be
1086
1025
                # 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])
 
1026
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
 
1027
                                   newpath_encoded, prop_str))
 
1028
            if changed_content:
 
1029
                self._diff(file_id, oldpath, newpath, kind[0], kind[1])
1091
1030
                has_changes = 1
1092
1031
            if renamed:
1093
1032
                has_changes = 1
1094
1033
        return has_changes
1095
1034
 
1096
 
    def diff(self, old_path, new_path):
 
1035
    def diff(self, file_id, old_path, new_path):
1097
1036
        """Perform a diff of a single file
1098
1037
 
 
1038
        :param file_id: file-id of the file
1099
1039
        :param old_path: The path of the file in the old tree
1100
1040
        :param new_path: The path of the file in the new tree
1101
1041
        """
1102
 
        if old_path is None:
 
1042
        try:
 
1043
            old_kind = self.old_tree.kind(file_id)
 
1044
        except (errors.NoSuchId, errors.NoSuchFile):
1103
1045
            old_kind = None
1104
 
        else:
1105
 
            old_kind = self.old_tree.kind(old_path)
1106
 
        if new_path is None:
 
1046
        try:
 
1047
            new_kind = self.new_tree.kind(file_id)
 
1048
        except (errors.NoSuchId, errors.NoSuchFile):
1107
1049
            new_kind = None
1108
 
        else:
1109
 
            new_kind = self.new_tree.kind(new_path)
1110
 
        self._diff(old_path, new_path, old_kind, new_kind)
1111
 
 
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)
 
1050
        self._diff(file_id, old_path, new_path, old_kind, new_kind)
 
1051
 
 
1052
 
 
1053
    def _diff(self, file_id, old_path, new_path, old_kind, new_kind):
 
1054
        result = DiffPath._diff_many(self.differs, file_id, old_path,
 
1055
                                       new_path, old_kind, new_kind)
1115
1056
        if result is DiffPath.CANNOT_DIFF:
1116
1057
            error_path = new_path
1117
1058
            if error_path is None: