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

Partially fix pull.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004, 2005, 2006 Canonical Ltd.
2
 
#
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
#
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
#
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
import difflib
18
 
import os
19
 
import re
20
 
import shutil
21
 
import sys
22
 
 
23
 
from bzrlib.lazy_import import lazy_import
24
 
lazy_import(globals(), """
25
 
import errno
26
 
import subprocess
27
 
import tempfile
28
 
import time
29
 
 
30
 
from bzrlib import (
31
 
    bzrdir,
32
 
    commands,
33
 
    errors,
34
 
    osutils,
35
 
    patiencediff,
36
 
    textfile,
37
 
    timestamp,
38
 
    )
39
 
""")
40
 
 
41
 
from bzrlib.symbol_versioning import (
42
 
        deprecated_function,
43
 
        one_zero,
44
 
        )
45
 
from bzrlib.trace import mutter, warning
46
 
 
47
 
 
48
 
# TODO: Rather than building a changeset object, we should probably
49
 
# invoke callbacks on an object.  That object can either accumulate a
50
 
# list, write them out directly, etc etc.
51
 
 
52
 
 
53
 
class _PrematchedMatcher(difflib.SequenceMatcher):
54
 
    """Allow SequenceMatcher operations to use predetermined blocks"""
55
 
 
56
 
    def __init__(self, matching_blocks):
57
 
        difflib.SequenceMatcher(self, None, None)
58
 
        self.matching_blocks = matching_blocks
59
 
        self.opcodes = None
60
 
 
61
 
 
62
 
def internal_diff(old_filename, oldlines, new_filename, newlines, to_file,
63
 
                  allow_binary=False, sequence_matcher=None,
64
 
                  path_encoding='utf8'):
65
 
    # FIXME: difflib is wrong if there is no trailing newline.
66
 
    # The syntax used by patch seems to be "\ No newline at
67
 
    # end of file" following the last diff line from that
68
 
    # file.  This is not trivial to insert into the
69
 
    # unified_diff output and it might be better to just fix
70
 
    # or replace that function.
71
 
 
72
 
    # In the meantime we at least make sure the patch isn't
73
 
    # mangled.
74
 
 
75
 
 
76
 
    # Special workaround for Python2.3, where difflib fails if
77
 
    # both sequences are empty.
78
 
    if not oldlines and not newlines:
79
 
        return
80
 
    
81
 
    if allow_binary is False:
82
 
        textfile.check_text_lines(oldlines)
83
 
        textfile.check_text_lines(newlines)
84
 
 
85
 
    if sequence_matcher is None:
86
 
        sequence_matcher = patiencediff.PatienceSequenceMatcher
87
 
    ud = patiencediff.unified_diff(oldlines, newlines,
88
 
                      fromfile=old_filename.encode(path_encoding),
89
 
                      tofile=new_filename.encode(path_encoding),
90
 
                      sequencematcher=sequence_matcher)
91
 
 
92
 
    ud = list(ud)
93
 
    if len(ud) == 0: # Identical contents, nothing to do
94
 
        return
95
 
    # work-around for difflib being too smart for its own good
96
 
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
97
 
    if not oldlines:
98
 
        ud[2] = ud[2].replace('-1,0', '-0,0')
99
 
    elif not newlines:
100
 
        ud[2] = ud[2].replace('+1,0', '+0,0')
101
 
    # work around for difflib emitting random spaces after the label
102
 
    ud[0] = ud[0][:-2] + '\n'
103
 
    ud[1] = ud[1][:-2] + '\n'
104
 
 
105
 
    for line in ud:
106
 
        to_file.write(line)
107
 
        if not line.endswith('\n'):
108
 
            to_file.write("\n\\ No newline at end of file\n")
109
 
    to_file.write('\n')
110
 
 
111
 
 
112
 
def _spawn_external_diff(diffcmd, capture_errors=True):
113
 
    """Spawn the externall diff process, and return the child handle.
114
 
 
115
 
    :param diffcmd: The command list to spawn
116
 
    :param capture_errors: Capture stderr as well as setting LANG=C
117
 
        and LC_ALL=C. This lets us read and understand the output of diff,
118
 
        and respond to any errors.
119
 
    :return: A Popen object.
120
 
    """
121
 
    if capture_errors:
122
 
        # construct minimal environment
123
 
        env = {}
124
 
        path = os.environ.get('PATH')
125
 
        if path is not None:
126
 
            env['PATH'] = path
127
 
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
128
 
        env['LANG'] = 'C'
129
 
        env['LC_ALL'] = 'C'
130
 
        stderr = subprocess.PIPE
131
 
    else:
132
 
        env = None
133
 
        stderr = None
134
 
 
135
 
    try:
136
 
        pipe = subprocess.Popen(diffcmd,
137
 
                                stdin=subprocess.PIPE,
138
 
                                stdout=subprocess.PIPE,
139
 
                                stderr=stderr,
140
 
                                env=env)
141
 
    except OSError, e:
142
 
        if e.errno == errno.ENOENT:
143
 
            raise errors.NoDiff(str(e))
144
 
        raise
145
 
 
146
 
    return pipe
147
 
 
148
 
 
149
 
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
150
 
                  diff_opts):
151
 
    """Display a diff by calling out to the external diff program."""
152
 
    # make sure our own output is properly ordered before the diff
153
 
    to_file.flush()
154
 
 
155
 
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
156
 
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
157
 
    oldtmpf = os.fdopen(oldtmp_fd, 'wb')
158
 
    newtmpf = os.fdopen(newtmp_fd, 'wb')
159
 
 
160
 
    try:
161
 
        # TODO: perhaps a special case for comparing to or from the empty
162
 
        # sequence; can just use /dev/null on Unix
163
 
 
164
 
        # TODO: if either of the files being compared already exists as a
165
 
        # regular named file (e.g. in the working directory) then we can
166
 
        # compare directly to that, rather than copying it.
167
 
 
168
 
        oldtmpf.writelines(oldlines)
169
 
        newtmpf.writelines(newlines)
170
 
 
171
 
        oldtmpf.close()
172
 
        newtmpf.close()
173
 
 
174
 
        if not diff_opts:
175
 
            diff_opts = []
176
 
        diffcmd = ['diff',
177
 
                   '--label', old_filename,
178
 
                   old_abspath,
179
 
                   '--label', new_filename,
180
 
                   new_abspath,
181
 
                   '--binary',
182
 
                  ]
183
 
 
184
 
        # diff only allows one style to be specified; they don't override.
185
 
        # note that some of these take optargs, and the optargs can be
186
 
        # directly appended to the options.
187
 
        # this is only an approximate parser; it doesn't properly understand
188
 
        # the grammar.
189
 
        for s in ['-c', '-u', '-C', '-U',
190
 
                  '-e', '--ed',
191
 
                  '-q', '--brief',
192
 
                  '--normal',
193
 
                  '-n', '--rcs',
194
 
                  '-y', '--side-by-side',
195
 
                  '-D', '--ifdef']:
196
 
            for j in diff_opts:
197
 
                if j.startswith(s):
198
 
                    break
199
 
            else:
200
 
                continue
201
 
            break
202
 
        else:
203
 
            diffcmd.append('-u')
204
 
                  
205
 
        if diff_opts:
206
 
            diffcmd.extend(diff_opts)
207
 
 
208
 
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
209
 
        out,err = pipe.communicate()
210
 
        rc = pipe.returncode
211
 
        
212
 
        # internal_diff() adds a trailing newline, add one here for consistency
213
 
        out += '\n'
214
 
        if rc == 2:
215
 
            # 'diff' gives retcode == 2 for all sorts of errors
216
 
            # one of those is 'Binary files differ'.
217
 
            # Bad options could also be the problem.
218
 
            # 'Binary files' is not a real error, so we suppress that error.
219
 
            lang_c_out = out
220
 
 
221
 
            # Since we got here, we want to make sure to give an i18n error
222
 
            pipe = _spawn_external_diff(diffcmd, capture_errors=False)
223
 
            out, err = pipe.communicate()
224
 
 
225
 
            # Write out the new i18n diff response
226
 
            to_file.write(out+'\n')
227
 
            if pipe.returncode != 2:
228
 
                raise errors.BzrError(
229
 
                               'external diff failed with exit code 2'
230
 
                               ' when run with LANG=C and LC_ALL=C,'
231
 
                               ' but not when run natively: %r' % (diffcmd,))
232
 
 
233
 
            first_line = lang_c_out.split('\n', 1)[0]
234
 
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
235
 
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
236
 
            if m is None:
237
 
                raise errors.BzrError('external diff failed with exit code 2;'
238
 
                                      ' command: %r' % (diffcmd,))
239
 
            else:
240
 
                # Binary files differ, just return
241
 
                return
242
 
 
243
 
        # If we got to here, we haven't written out the output of diff
244
 
        # do so now
245
 
        to_file.write(out)
246
 
        if rc not in (0, 1):
247
 
            # returns 1 if files differ; that's OK
248
 
            if rc < 0:
249
 
                msg = 'signal %d' % (-rc)
250
 
            else:
251
 
                msg = 'exit code %d' % rc
252
 
                
253
 
            raise errors.BzrError('external diff failed with %s; command: %r' 
254
 
                                  % (rc, diffcmd))
255
 
 
256
 
 
257
 
    finally:
258
 
        oldtmpf.close()                 # and delete
259
 
        newtmpf.close()
260
 
        # Clean up. Warn in case the files couldn't be deleted
261
 
        # (in case windows still holds the file open, but not
262
 
        # if the files have already been deleted)
263
 
        try:
264
 
            os.remove(old_abspath)
265
 
        except OSError, e:
266
 
            if e.errno not in (errno.ENOENT,):
267
 
                warning('Failed to delete temporary file: %s %s',
268
 
                        old_abspath, e)
269
 
        try:
270
 
            os.remove(new_abspath)
271
 
        except OSError:
272
 
            if e.errno not in (errno.ENOENT,):
273
 
                warning('Failed to delete temporary file: %s %s',
274
 
                        new_abspath, e)
275
 
 
276
 
 
277
 
@deprecated_function(one_zero)
278
 
def diff_cmd_helper(tree, specific_files, external_diff_options, 
279
 
                    old_revision_spec=None, new_revision_spec=None,
280
 
                    revision_specs=None,
281
 
                    old_label='a/', new_label='b/'):
282
 
    """Helper for cmd_diff.
283
 
 
284
 
    :param tree:
285
 
        A WorkingTree
286
 
 
287
 
    :param specific_files:
288
 
        The specific files to compare, or None
289
 
 
290
 
    :param external_diff_options:
291
 
        If non-None, run an external diff, and pass it these options
292
 
 
293
 
    :param old_revision_spec:
294
 
        If None, use basis tree as old revision, otherwise use the tree for
295
 
        the specified revision. 
296
 
 
297
 
    :param new_revision_spec:
298
 
        If None, use working tree as new revision, otherwise use the tree for
299
 
        the specified revision.
300
 
    
301
 
    :param revision_specs: 
302
 
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
303
 
        to compare.  This can be passed as an alternative to the old_revision_spec 
304
 
        and new_revision_spec parameters.
305
 
 
306
 
    The more general form is show_diff_trees(), where the caller
307
 
    supplies any two trees.
308
 
    """
309
 
 
310
 
    # TODO: perhaps remove the old parameters old_revision_spec and
311
 
    # new_revision_spec, since this is only really for use from cmd_diff and
312
 
    # it now always passes through a sequence of revision_specs -- mbp
313
 
    # 20061221
314
 
 
315
 
    def spec_tree(spec):
316
 
        if tree:
317
 
            revision = spec.in_store(tree.branch)
318
 
        else:
319
 
            revision = spec.in_store(None)
320
 
        revision_id = revision.rev_id
321
 
        branch = revision.branch
322
 
        return branch.repository.revision_tree(revision_id)
323
 
 
324
 
    if revision_specs is not None:
325
 
        assert (old_revision_spec is None
326
 
                and new_revision_spec is None)
327
 
        if len(revision_specs) > 0:
328
 
            old_revision_spec = revision_specs[0]
329
 
        if len(revision_specs) > 1:
330
 
            new_revision_spec = revision_specs[1]
331
 
 
332
 
    if old_revision_spec is None:
333
 
        old_tree = tree.basis_tree()
334
 
    else:
335
 
        old_tree = spec_tree(old_revision_spec)
336
 
 
337
 
    if (new_revision_spec is None
338
 
        or new_revision_spec.spec is None):
339
 
        new_tree = tree
340
 
    else:
341
 
        new_tree = spec_tree(new_revision_spec)
342
 
 
343
 
    if new_tree is not tree:
344
 
        extra_trees = (tree,)
345
 
    else:
346
 
        extra_trees = None
347
 
 
348
 
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
349
 
                           external_diff_options,
350
 
                           old_label=old_label, new_label=new_label,
351
 
                           extra_trees=extra_trees)
352
 
 
353
 
 
354
 
def _get_trees_to_diff(path_list, revision_specs, old_url, new_url):
355
 
    """Get the trees and specific files to diff given a list of paths.
356
 
 
357
 
    This method works out the trees to be diff'ed and the files of
358
 
    interest within those trees.
359
 
 
360
 
    :param path_list:
361
 
        the list of arguments passed to the diff command
362
 
    :param revision_specs:
363
 
        Zero, one or two RevisionSpecs from the diff command line,
364
 
        saying what revisions to compare.
365
 
    :param old_url:
366
 
        The url of the old branch or tree. If None, the tree to use is
367
 
        taken from the first path, if any, or the current working tree.
368
 
    :param new_url:
369
 
        The url of the new branch or tree. If None, the tree to use is
370
 
        taken from the first path, if any, or the current working tree.
371
 
    :returns:
372
 
        a tuple of (old_tree, new_tree, specific_files, extra_trees) where
373
 
        extra_trees is a sequence of additional trees to search in for
374
 
        file-ids.
375
 
    """
376
 
    # Get the old and new revision specs
377
 
    old_revision_spec = None
378
 
    new_revision_spec = None
379
 
    if revision_specs is not None:
380
 
        if len(revision_specs) > 0:
381
 
            old_revision_spec = revision_specs[0]
382
 
            if old_url is None:
383
 
                old_url = old_revision_spec.get_branch()
384
 
        if len(revision_specs) > 1:
385
 
            new_revision_spec = revision_specs[1]
386
 
            if new_url is None:
387
 
                new_url = new_revision_spec.get_branch()
388
 
 
389
 
    other_paths = []
390
 
    make_paths_wt_relative = True
391
 
    consider_relpath = True
392
 
    if path_list is None or len(path_list) == 0:
393
 
        # If no path is given, the current working tree is used
394
 
        default_location = u'.'
395
 
        consider_relpath = False
396
 
    elif old_url is not None and new_url is not None:
397
 
        other_paths = path_list
398
 
        make_paths_wt_relative = False
399
 
    else:
400
 
        default_location = path_list[0]
401
 
        other_paths = path_list[1:]
402
 
 
403
 
    # Get the old location
404
 
    specific_files = []
405
 
    if old_url is None:
406
 
        old_url = default_location
407
 
    working_tree, branch, relpath = \
408
 
        bzrdir.BzrDir.open_containing_tree_or_branch(old_url)
409
 
    if consider_relpath and relpath != '':
410
 
        specific_files.append(relpath)
411
 
    old_tree = _get_tree_to_diff(old_revision_spec, working_tree, branch)
412
 
 
413
 
    # Get the new location
414
 
    if new_url is None:
415
 
        new_url = default_location
416
 
    if new_url != old_url:
417
 
        working_tree, branch, relpath = \
418
 
            bzrdir.BzrDir.open_containing_tree_or_branch(new_url)
419
 
        if consider_relpath and relpath != '':
420
 
            specific_files.append(relpath)
421
 
    new_tree = _get_tree_to_diff(new_revision_spec, working_tree, branch,
422
 
        basis_is_default=working_tree is None)
423
 
 
424
 
    # Get the specific files (all files is None, no files is [])
425
 
    if make_paths_wt_relative and working_tree is not None:
426
 
        other_paths = _relative_paths_in_tree(working_tree, other_paths)
427
 
    specific_files.extend(other_paths)
428
 
    if len(specific_files) == 0:
429
 
        specific_files = None
430
 
 
431
 
    # Get extra trees that ought to be searched for file-ids
432
 
    extra_trees = None
433
 
    if working_tree is not None and working_tree not in (old_tree, new_tree):
434
 
        extra_trees = (working_tree,)
435
 
    return old_tree, new_tree, specific_files, extra_trees
436
 
 
437
 
 
438
 
def _get_tree_to_diff(spec, tree=None, branch=None, basis_is_default=True):
439
 
    if branch is None and tree is not None:
440
 
        branch = tree.branch
441
 
    if spec is None or spec.spec is None:
442
 
        if basis_is_default:
443
 
            if tree is not None:
444
 
                return tree.basis_tree()
445
 
            else:
446
 
                return branch.basis_tree()
447
 
        else:
448
 
            return tree
449
 
    revision = spec.in_store(branch)
450
 
    revision_id = revision.rev_id
451
 
    rev_branch = revision.branch
452
 
    return rev_branch.repository.revision_tree(revision_id)
453
 
 
454
 
 
455
 
def _relative_paths_in_tree(tree, paths):
456
 
    """Get the relative paths within a working tree.
457
 
 
458
 
    Each path may be either an absolute path or a path relative to the
459
 
    current working directory.
460
 
    """
461
 
    result = []
462
 
    for filename in paths:
463
 
        try:
464
 
            result.append(tree.relpath(osutils.dereference_path(filename)))
465
 
        except errors.PathNotChild:
466
 
            raise errors.BzrCommandError("Files are in different branches")
467
 
    return result
468
 
 
469
 
 
470
 
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
471
 
                    external_diff_options=None,
472
 
                    old_label='a/', new_label='b/',
473
 
                    extra_trees=None,
474
 
                    path_encoding='utf8',
475
 
                    using=None):
476
 
    """Show in text form the changes from one tree to another.
477
 
 
478
 
    to_file
479
 
        The output stream.
480
 
 
481
 
    specific_files
482
 
        Include only changes to these files - None for all changes.
483
 
 
484
 
    external_diff_options
485
 
        If set, use an external GNU diff and pass these options.
486
 
 
487
 
    extra_trees
488
 
        If set, more Trees to use for looking up file ids
489
 
 
490
 
    path_encoding
491
 
        If set, the path will be encoded as specified, otherwise is supposed
492
 
        to be utf8
493
 
    """
494
 
    old_tree.lock_read()
495
 
    try:
496
 
        if extra_trees is not None:
497
 
            for tree in extra_trees:
498
 
                tree.lock_read()
499
 
        new_tree.lock_read()
500
 
        try:
501
 
            differ = DiffTree.from_trees_options(old_tree, new_tree, to_file,
502
 
                                                 path_encoding,
503
 
                                                 external_diff_options,
504
 
                                                 old_label, new_label, using)
505
 
            return differ.show_diff(specific_files, extra_trees)
506
 
        finally:
507
 
            new_tree.unlock()
508
 
            if extra_trees is not None:
509
 
                for tree in extra_trees:
510
 
                    tree.unlock()
511
 
    finally:
512
 
        old_tree.unlock()
513
 
 
514
 
 
515
 
def _patch_header_date(tree, file_id, path):
516
 
    """Returns a timestamp suitable for use in a patch header."""
517
 
    mtime = tree.get_file_mtime(file_id, path)
518
 
    assert mtime is not None, \
519
 
        "got an mtime of None for file-id %s, path %s in tree %s" % (
520
 
                file_id, path, tree)
521
 
    return timestamp.format_patch_date(mtime)
522
 
 
523
 
 
524
 
def _raise_if_nonexistent(paths, old_tree, new_tree):
525
 
    """Complain if paths are not in either inventory or tree.
526
 
 
527
 
    It's OK with the files exist in either tree's inventory, or 
528
 
    if they exist in the tree but are not versioned.
529
 
    
530
 
    This can be used by operations such as bzr status that can accept
531
 
    unknown or ignored files.
532
 
    """
533
 
    mutter("check paths: %r", paths)
534
 
    if not paths:
535
 
        return
536
 
    s = old_tree.filter_unversioned_files(paths)
537
 
    s = new_tree.filter_unversioned_files(s)
538
 
    s = [path for path in s if not new_tree.has_filename(path)]
539
 
    if s:
540
 
        raise errors.PathsDoNotExist(sorted(s))
541
 
 
542
 
 
543
 
def get_prop_change(meta_modified):
544
 
    if meta_modified:
545
 
        return " (properties changed)"
546
 
    else:
547
 
        return  ""
548
 
 
549
 
 
550
 
class DiffPath(object):
551
 
    """Base type for command object that compare files"""
552
 
 
553
 
    # The type or contents of the file were unsuitable for diffing
554
 
    CANNOT_DIFF = 'CANNOT_DIFF'
555
 
    # The file has changed in a semantic way
556
 
    CHANGED = 'CHANGED'
557
 
    # The file content may have changed, but there is no semantic change
558
 
    UNCHANGED = 'UNCHANGED'
559
 
 
560
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8'):
561
 
        """Constructor.
562
 
 
563
 
        :param old_tree: The tree to show as the old tree in the comparison
564
 
        :param new_tree: The tree to show as new in the comparison
565
 
        :param to_file: The file to write comparison data to
566
 
        :param path_encoding: The character encoding to write paths in
567
 
        """
568
 
        self.old_tree = old_tree
569
 
        self.new_tree = new_tree
570
 
        self.to_file = to_file
571
 
        self.path_encoding = path_encoding
572
 
 
573
 
    def finish(self):
574
 
        pass
575
 
 
576
 
    @classmethod
577
 
    def from_diff_tree(klass, diff_tree):
578
 
        return klass(diff_tree.old_tree, diff_tree.new_tree,
579
 
                     diff_tree.to_file, diff_tree.path_encoding)
580
 
 
581
 
    @staticmethod
582
 
    def _diff_many(differs, file_id, old_path, new_path, old_kind, new_kind):
583
 
        for file_differ in differs:
584
 
            result = file_differ.diff(file_id, old_path, new_path, old_kind,
585
 
                                      new_kind)
586
 
            if result is not DiffPath.CANNOT_DIFF:
587
 
                return result
588
 
        else:
589
 
            return DiffPath.CANNOT_DIFF
590
 
 
591
 
 
592
 
class DiffKindChange(object):
593
 
    """Special differ for file kind changes.
594
 
 
595
 
    Represents kind change as deletion + creation.  Uses the other differs
596
 
    to do this.
597
 
    """
598
 
    def __init__(self, differs):
599
 
        self.differs = differs
600
 
 
601
 
    def finish(self):
602
 
        pass
603
 
 
604
 
    @classmethod
605
 
    def from_diff_tree(klass, diff_tree):
606
 
        return klass(diff_tree.differs)
607
 
 
608
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
609
 
        """Perform comparison
610
 
 
611
 
        :param file_id: The file_id of the file to compare
612
 
        :param old_path: Path of the file in the old tree
613
 
        :param new_path: Path of the file in the new tree
614
 
        :param old_kind: Old file-kind of the file
615
 
        :param new_kind: New file-kind of the file
616
 
        """
617
 
        if None in (old_kind, new_kind):
618
 
            return DiffPath.CANNOT_DIFF
619
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
620
 
                                       new_path, old_kind, None)
621
 
        if result is DiffPath.CANNOT_DIFF:
622
 
            return result
623
 
        return DiffPath._diff_many(self.differs, file_id, old_path, new_path,
624
 
                                     None, new_kind)
625
 
 
626
 
 
627
 
class DiffDirectory(DiffPath):
628
 
 
629
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
630
 
        """Perform comparison between two directories.  (dummy)
631
 
 
632
 
        """
633
 
        if 'directory' not in (old_kind, new_kind):
634
 
            return self.CANNOT_DIFF
635
 
        if old_kind not in ('directory', None):
636
 
            return self.CANNOT_DIFF
637
 
        if new_kind not in ('directory', None):
638
 
            return self.CANNOT_DIFF
639
 
        return self.CHANGED
640
 
 
641
 
 
642
 
class DiffSymlink(DiffPath):
643
 
 
644
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
645
 
        """Perform comparison between two symlinks
646
 
 
647
 
        :param file_id: The file_id of the file to compare
648
 
        :param old_path: Path of the file in the old tree
649
 
        :param new_path: Path of the file in the new tree
650
 
        :param old_kind: Old file-kind of the file
651
 
        :param new_kind: New file-kind of the file
652
 
        """
653
 
        if 'symlink' not in (old_kind, new_kind):
654
 
            return self.CANNOT_DIFF
655
 
        if old_kind == 'symlink':
656
 
            old_target = self.old_tree.get_symlink_target(file_id)
657
 
        elif old_kind is None:
658
 
            old_target = None
659
 
        else:
660
 
            return self.CANNOT_DIFF
661
 
        if new_kind == 'symlink':
662
 
            new_target = self.new_tree.get_symlink_target(file_id)
663
 
        elif new_kind is None:
664
 
            new_target = None
665
 
        else:
666
 
            return self.CANNOT_DIFF
667
 
        return self.diff_symlink(old_target, new_target)
668
 
 
669
 
    def diff_symlink(self, old_target, new_target):
670
 
        if old_target is None:
671
 
            self.to_file.write('=== target is %r\n' % new_target)
672
 
        elif new_target is None:
673
 
            self.to_file.write('=== target was %r\n' % old_target)
674
 
        else:
675
 
            self.to_file.write('=== target changed %r => %r\n' %
676
 
                              (old_target, new_target))
677
 
        return self.CHANGED
678
 
 
679
 
 
680
 
class DiffText(DiffPath):
681
 
 
682
 
    # GNU Patch uses the epoch date to detect files that are being added
683
 
    # or removed in a diff.
684
 
    EPOCH_DATE = '1970-01-01 00:00:00 +0000'
685
 
 
686
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
687
 
                 old_label='', new_label='', text_differ=internal_diff):
688
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
689
 
        self.text_differ = text_differ
690
 
        self.old_label = old_label
691
 
        self.new_label = new_label
692
 
        self.path_encoding = path_encoding
693
 
 
694
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
695
 
        """Compare two files in unified diff format
696
 
 
697
 
        :param file_id: The file_id of the file to compare
698
 
        :param old_path: Path of the file in the old tree
699
 
        :param new_path: Path of the file in the new tree
700
 
        :param old_kind: Old file-kind of the file
701
 
        :param new_kind: New file-kind of the file
702
 
        """
703
 
        if 'file' not in (old_kind, new_kind):
704
 
            return self.CANNOT_DIFF
705
 
        from_file_id = to_file_id = file_id
706
 
        if old_kind == 'file':
707
 
            old_date = _patch_header_date(self.old_tree, file_id, old_path)
708
 
        elif old_kind is None:
709
 
            old_date = self.EPOCH_DATE
710
 
            from_file_id = None
711
 
        else:
712
 
            return self.CANNOT_DIFF
713
 
        if new_kind == 'file':
714
 
            new_date = _patch_header_date(self.new_tree, file_id, new_path)
715
 
        elif new_kind is None:
716
 
            new_date = self.EPOCH_DATE
717
 
            to_file_id = None
718
 
        else:
719
 
            return self.CANNOT_DIFF
720
 
        from_label = '%s%s\t%s' % (self.old_label, old_path, old_date)
721
 
        to_label = '%s%s\t%s' % (self.new_label, new_path, new_date)
722
 
        return self.diff_text(from_file_id, to_file_id, from_label, to_label)
723
 
 
724
 
    def diff_text(self, from_file_id, to_file_id, from_label, to_label):
725
 
        """Diff the content of given files in two trees
726
 
 
727
 
        :param from_file_id: The id of the file in the from tree.  If None,
728
 
            the file is not present in the from tree.
729
 
        :param to_file_id: The id of the file in the to tree.  This may refer
730
 
            to a different file from from_file_id.  If None,
731
 
            the file is not present in the to tree.
732
 
        """
733
 
        def _get_text(tree, file_id):
734
 
            if file_id is not None:
735
 
                return tree.get_file(file_id).readlines()
736
 
            else:
737
 
                return []
738
 
        try:
739
 
            from_text = _get_text(self.old_tree, from_file_id)
740
 
            to_text = _get_text(self.new_tree, to_file_id)
741
 
            self.text_differ(from_label, from_text, to_label, to_text,
742
 
                             self.to_file)
743
 
        except errors.BinaryFile:
744
 
            self.to_file.write(
745
 
                  ("Binary files %s and %s differ\n" %
746
 
                  (from_label, to_label)).encode(self.path_encoding))
747
 
        return self.CHANGED
748
 
 
749
 
 
750
 
class DiffFromTool(DiffPath):
751
 
 
752
 
    def __init__(self, command_template, old_tree, new_tree, to_file,
753
 
                 path_encoding='utf-8'):
754
 
        DiffPath.__init__(self, old_tree, new_tree, to_file, path_encoding)
755
 
        self.command_template = command_template
756
 
        self._root = tempfile.mkdtemp(prefix='bzr-diff-')
757
 
 
758
 
    @classmethod
759
 
    def from_string(klass, command_string, old_tree, new_tree, to_file,
760
 
                    path_encoding='utf-8'):
761
 
        command_template = commands.shlex_split_unicode(command_string)
762
 
        command_template.extend(['%(old_path)s', '%(new_path)s'])
763
 
        return klass(command_template, old_tree, new_tree, to_file,
764
 
                     path_encoding)
765
 
 
766
 
    @classmethod
767
 
    def make_from_diff_tree(klass, command_string):
768
 
        def from_diff_tree(diff_tree):
769
 
            return klass.from_string(command_string, diff_tree.old_tree,
770
 
                                     diff_tree.new_tree, diff_tree.to_file)
771
 
        return from_diff_tree
772
 
 
773
 
    def _get_command(self, old_path, new_path):
774
 
        my_map = {'old_path': old_path, 'new_path': new_path}
775
 
        return [t % my_map for t in self.command_template]
776
 
 
777
 
    def _execute(self, old_path, new_path):
778
 
        command = self._get_command(old_path, new_path)
779
 
        try:
780
 
            proc = subprocess.Popen(command, stdout=subprocess.PIPE,
781
 
                                    cwd=self._root)
782
 
        except OSError, e:
783
 
            if e.errno == errno.ENOENT:
784
 
                raise errors.ExecutableMissing(command[0])
785
 
            else:
786
 
                raise
787
 
        self.to_file.write(proc.stdout.read())
788
 
        return proc.wait()
789
 
 
790
 
    def _try_symlink_root(self, tree, prefix):
791
 
        if not (getattr(tree, 'abspath', None) is not None
792
 
                and osutils.has_symlinks()):
793
 
            return False
794
 
        try:
795
 
            os.symlink(tree.abspath(''), osutils.pathjoin(self._root, prefix))
796
 
        except OSError, e:
797
 
            if e.errno != errno.EEXIST:
798
 
                raise
799
 
        return True
800
 
 
801
 
    def _write_file(self, file_id, tree, prefix, relpath):
802
 
        full_path = osutils.pathjoin(self._root, prefix, relpath)
803
 
        if self._try_symlink_root(tree, prefix):
804
 
            return full_path
805
 
        parent_dir = osutils.dirname(full_path)
806
 
        try:
807
 
            os.makedirs(parent_dir)
808
 
        except OSError, e:
809
 
            if e.errno != errno.EEXIST:
810
 
                raise
811
 
        source = tree.get_file(file_id, relpath)
812
 
        try:
813
 
            target = open(full_path, 'wb')
814
 
            try:
815
 
                osutils.pumpfile(source, target)
816
 
            finally:
817
 
                target.close()
818
 
        finally:
819
 
            source.close()
820
 
        osutils.make_readonly(full_path)
821
 
        mtime = tree.get_file_mtime(file_id)
822
 
        os.utime(full_path, (mtime, mtime))
823
 
        return full_path
824
 
 
825
 
    def _prepare_files(self, file_id, old_path, new_path):
826
 
        old_disk_path = self._write_file(file_id, self.old_tree, 'old',
827
 
                                         old_path)
828
 
        new_disk_path = self._write_file(file_id, self.new_tree, 'new',
829
 
                                         new_path)
830
 
        return old_disk_path, new_disk_path
831
 
 
832
 
    def finish(self):
833
 
        osutils.rmtree(self._root)
834
 
 
835
 
    def diff(self, file_id, old_path, new_path, old_kind, new_kind):
836
 
        if (old_kind, new_kind) != ('file', 'file'):
837
 
            return DiffPath.CANNOT_DIFF
838
 
        self._prepare_files(file_id, old_path, new_path)
839
 
        self._execute(osutils.pathjoin('old', old_path),
840
 
                      osutils.pathjoin('new', new_path))
841
 
 
842
 
 
843
 
class DiffTree(object):
844
 
    """Provides textual representations of the difference between two trees.
845
 
 
846
 
    A DiffTree examines two trees and where a file-id has altered
847
 
    between them, generates a textual representation of the difference.
848
 
    DiffTree uses a sequence of DiffPath objects which are each
849
 
    given the opportunity to handle a given altered fileid. The list
850
 
    of DiffPath objects can be extended globally by appending to
851
 
    DiffTree.diff_factories, or for a specific diff operation by
852
 
    supplying the extra_factories option to the appropriate method.
853
 
    """
854
 
 
855
 
    # list of factories that can provide instances of DiffPath objects
856
 
    # may be extended by plugins.
857
 
    diff_factories = [DiffSymlink.from_diff_tree,
858
 
                      DiffDirectory.from_diff_tree]
859
 
 
860
 
    def __init__(self, old_tree, new_tree, to_file, path_encoding='utf-8',
861
 
                 diff_text=None, extra_factories=None):
862
 
        """Constructor
863
 
 
864
 
        :param old_tree: Tree to show as old in the comparison
865
 
        :param new_tree: Tree to show as new in the comparison
866
 
        :param to_file: File to write comparision to
867
 
        :param path_encoding: Character encoding to write paths in
868
 
        :param diff_text: DiffPath-type object to use as a last resort for
869
 
            diffing text files.
870
 
        :param extra_factories: Factories of DiffPaths to try before any other
871
 
            DiffPaths"""
872
 
        if diff_text is None:
873
 
            diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
874
 
                                 '', '',  internal_diff)
875
 
        self.old_tree = old_tree
876
 
        self.new_tree = new_tree
877
 
        self.to_file = to_file
878
 
        self.path_encoding = path_encoding
879
 
        self.differs = []
880
 
        if extra_factories is not None:
881
 
            self.differs.extend(f(self) for f in extra_factories)
882
 
        self.differs.extend(f(self) for f in self.diff_factories)
883
 
        self.differs.extend([diff_text, DiffKindChange.from_diff_tree(self)])
884
 
 
885
 
    @classmethod
886
 
    def from_trees_options(klass, old_tree, new_tree, to_file,
887
 
                           path_encoding, external_diff_options, old_label,
888
 
                           new_label, using):
889
 
        """Factory for producing a DiffTree.
890
 
 
891
 
        Designed to accept options used by show_diff_trees.
892
 
        :param old_tree: The tree to show as old in the comparison
893
 
        :param new_tree: The tree to show as new in the comparison
894
 
        :param to_file: File to write comparisons to
895
 
        :param path_encoding: Character encoding to use for writing paths
896
 
        :param external_diff_options: If supplied, use the installed diff
897
 
            binary to perform file comparison, using supplied options.
898
 
        :param old_label: Prefix to use for old file labels
899
 
        :param new_label: Prefix to use for new file labels
900
 
        :param using: Commandline to use to invoke an external diff tool
901
 
        """
902
 
        if using is not None:
903
 
            extra_factories = [DiffFromTool.make_from_diff_tree(using)]
904
 
        else:
905
 
            extra_factories = []
906
 
        if external_diff_options:
907
 
            assert isinstance(external_diff_options, basestring)
908
 
            opts = external_diff_options.split()
909
 
            def diff_file(olab, olines, nlab, nlines, to_file):
910
 
                external_diff(olab, olines, nlab, nlines, to_file, opts)
911
 
        else:
912
 
            diff_file = internal_diff
913
 
        diff_text = DiffText(old_tree, new_tree, to_file, path_encoding,
914
 
                             old_label, new_label, diff_file)
915
 
        return klass(old_tree, new_tree, to_file, path_encoding, diff_text,
916
 
                     extra_factories)
917
 
 
918
 
    def show_diff(self, specific_files, extra_trees=None):
919
 
        """Write tree diff to self.to_file
920
 
 
921
 
        :param sepecific_files: the specific files to compare (recursive)
922
 
        :param extra_trees: extra trees to use for mapping paths to file_ids
923
 
        """
924
 
        try:
925
 
            return self._show_diff(specific_files, extra_trees)
926
 
        finally:
927
 
            for differ in self.differs:
928
 
                differ.finish()
929
 
 
930
 
    def _show_diff(self, specific_files, extra_trees):
931
 
        # TODO: Generation of pseudo-diffs for added/deleted files could
932
 
        # be usefully made into a much faster special case.
933
 
        iterator = self.new_tree._iter_changes(self.old_tree,
934
 
                                               specific_files=specific_files,
935
 
                                               extra_trees=extra_trees,
936
 
                                               require_versioned=True)
937
 
        has_changes = 0
938
 
        def changes_key(change):
939
 
            old_path, new_path = change[1]
940
 
            path = new_path
941
 
            if path is None:
942
 
                path = old_path
943
 
            return path
944
 
        def get_encoded_path(path):
945
 
            if path is not None:
946
 
                return path.encode(self.path_encoding, "replace")
947
 
        for (file_id, paths, changed_content, versioned, parent, name, kind,
948
 
             executable) in sorted(iterator, key=changes_key):
949
 
            if parent == (None, None):
950
 
                continue
951
 
            oldpath, newpath = paths
952
 
            oldpath_encoded = get_encoded_path(paths[0])
953
 
            newpath_encoded = get_encoded_path(paths[1])
954
 
            old_present = (kind[0] is not None and versioned[0])
955
 
            new_present = (kind[1] is not None and versioned[1])
956
 
            renamed = (parent[0], name[0]) != (parent[1], name[1])
957
 
            prop_str = get_prop_change(executable[0] != executable[1])
958
 
            if (old_present, new_present) == (True, False):
959
 
                self.to_file.write("=== removed %s '%s'\n" %
960
 
                                   (kind[0], oldpath_encoded))
961
 
                newpath = oldpath
962
 
            elif (old_present, new_present) == (False, True):
963
 
                self.to_file.write("=== added %s '%s'\n" %
964
 
                                   (kind[1], newpath_encoded))
965
 
                oldpath = newpath
966
 
            elif renamed:
967
 
                self.to_file.write("=== renamed %s '%s' => '%s'%s\n" %
968
 
                    (kind[0], oldpath_encoded, newpath_encoded, prop_str))
969
 
            else:
970
 
                # if it was produced by _iter_changes, it must be
971
 
                # modified *somehow*, either content or execute bit.
972
 
                self.to_file.write("=== modified %s '%s'%s\n" % (kind[0],
973
 
                                   newpath_encoded, prop_str))
974
 
            if changed_content:
975
 
                self.diff(file_id, oldpath, newpath)
976
 
                has_changes = 1
977
 
            if renamed:
978
 
                has_changes = 1
979
 
        return has_changes
980
 
 
981
 
    def diff(self, file_id, old_path, new_path):
982
 
        """Perform a diff of a single file
983
 
 
984
 
        :param file_id: file-id of the file
985
 
        :param old_path: The path of the file in the old tree
986
 
        :param new_path: The path of the file in the new tree
987
 
        """
988
 
        try:
989
 
            old_kind = self.old_tree.kind(file_id)
990
 
        except (errors.NoSuchId, errors.NoSuchFile):
991
 
            old_kind = None
992
 
        try:
993
 
            new_kind = self.new_tree.kind(file_id)
994
 
        except (errors.NoSuchId, errors.NoSuchFile):
995
 
            new_kind = None
996
 
 
997
 
        result = DiffPath._diff_many(self.differs, file_id, old_path,
998
 
                                       new_path, old_kind, new_kind)
999
 
        if result is DiffPath.CANNOT_DIFF:
1000
 
            error_path = new_path
1001
 
            if error_path is None:
1002
 
                error_path = old_path
1003
 
            raise errors.NoDiffFound(error_path)