/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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2006-04-13 23:16:57 UTC
  • mfrom: (1662.1.1 bzr.mbp.integration)
  • Revision ID: pqm@pqm.ubuntu.com-20060413231657-bce3d67d3e7a4f2b
(mbp/olaf) push/pull/merge --remember improvements

Show diffs side-by-side

added added

removed removed

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