~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

  • Committer: Wouter van Heyst
  • Date: 2006-06-07 16:15:45 UTC
  • mto: This revision was merged to the branch mainline in revision 1752.
  • Revision ID: larstiq@larstiq.dyndns.org-20060607161545-e3f87ba37d1d7eb9
cleanup urlutils

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
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
import os
18
 
import re
19
 
import sys
20
 
 
21
 
from bzrlib.lazy_import import lazy_import
22
 
lazy_import(globals(), """
23
 
import errno
24
 
import subprocess
25
 
import tempfile
26
17
import time
27
18
 
28
 
from bzrlib import (
29
 
    errors,
30
 
    osutils,
31
 
    patiencediff,
32
 
    textfile,
33
 
    timestamp,
34
 
    )
35
 
""")
36
 
 
37
 
# compatability - plugins import compare_trees from diff!!!
38
 
# deprecated as of 0.10
39
19
from bzrlib.delta import compare_trees
40
 
from bzrlib.symbol_versioning import (
41
 
        deprecated_function,
42
 
        zero_eight,
43
 
        )
44
 
from bzrlib.trace import mutter, warning
 
20
from bzrlib.errors import BzrError
 
21
import bzrlib.errors as errors
 
22
from bzrlib.patiencediff import unified_diff
 
23
import bzrlib.patiencediff
 
24
from bzrlib.symbol_versioning import *
 
25
from bzrlib.textfile import check_text_lines
 
26
from bzrlib.trace import mutter
45
27
 
46
28
 
47
29
# TODO: Rather than building a changeset object, we should probably
68
50
        return
69
51
    
70
52
    if allow_binary is False:
71
 
        textfile.check_text_lines(oldlines)
72
 
        textfile.check_text_lines(newlines)
 
53
        check_text_lines(oldlines)
 
54
        check_text_lines(newlines)
73
55
 
74
56
    if sequence_matcher is None:
75
 
        sequence_matcher = patiencediff.PatienceSequenceMatcher
76
 
    ud = patiencediff.unified_diff(oldlines, newlines,
 
57
        sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
 
58
    ud = unified_diff(oldlines, newlines,
77
59
                      fromfile=old_filename.encode(path_encoding),
78
60
                      tofile=new_filename.encode(path_encoding),
79
61
                      sequencematcher=sequence_matcher)
96
78
    print >>to_file
97
79
 
98
80
 
99
 
def _spawn_external_diff(diffcmd, capture_errors=True):
100
 
    """Spawn the externall diff process, and return the child handle.
101
 
 
102
 
    :param diffcmd: The command list to spawn
103
 
    :param capture_errors: Capture stderr as well as setting LANG=C
104
 
        and LC_ALL=C. This lets us read and understand the output of diff,
105
 
        and respond to any errors.
106
 
    :return: A Popen object.
107
 
    """
108
 
    if capture_errors:
109
 
        # construct minimal environment
110
 
        env = {}
111
 
        path = os.environ.get('PATH')
112
 
        if path is not None:
113
 
            env['PATH'] = path
114
 
        env['LANGUAGE'] = 'C'   # on win32 only LANGUAGE has effect
115
 
        env['LANG'] = 'C'
116
 
        env['LC_ALL'] = 'C'
117
 
        stderr = subprocess.PIPE
118
 
    else:
119
 
        env = None
120
 
        stderr = None
121
 
 
122
 
    try:
123
 
        pipe = subprocess.Popen(diffcmd,
124
 
                                stdin=subprocess.PIPE,
125
 
                                stdout=subprocess.PIPE,
126
 
                                stderr=stderr,
127
 
                                env=env)
128
 
    except OSError, e:
129
 
        if e.errno == errno.ENOENT:
130
 
            raise errors.NoDiff(str(e))
131
 
        raise
132
 
 
133
 
    return pipe
134
 
 
135
 
 
136
81
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
137
82
                  diff_opts):
138
83
    """Display a diff by calling out to the external diff program."""
 
84
    import sys
 
85
    
 
86
    if to_file != sys.stdout:
 
87
        raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
 
88
                                  to_file)
 
89
 
139
90
    # make sure our own output is properly ordered before the diff
140
91
    to_file.flush()
141
92
 
142
 
    oldtmp_fd, old_abspath = tempfile.mkstemp(prefix='bzr-diff-old-')
143
 
    newtmp_fd, new_abspath = tempfile.mkstemp(prefix='bzr-diff-new-')
144
 
    oldtmpf = os.fdopen(oldtmp_fd, 'wb')
145
 
    newtmpf = os.fdopen(newtmp_fd, 'wb')
 
93
    from tempfile import NamedTemporaryFile
 
94
    import os
 
95
 
 
96
    oldtmpf = NamedTemporaryFile()
 
97
    newtmpf = NamedTemporaryFile()
146
98
 
147
99
    try:
148
100
        # TODO: perhaps a special case for comparing to or from the empty
155
107
        oldtmpf.writelines(oldlines)
156
108
        newtmpf.writelines(newlines)
157
109
 
158
 
        oldtmpf.close()
159
 
        newtmpf.close()
 
110
        oldtmpf.flush()
 
111
        newtmpf.flush()
160
112
 
161
113
        if not diff_opts:
162
114
            diff_opts = []
163
115
        diffcmd = ['diff',
164
116
                   '--label', old_filename,
165
 
                   old_abspath,
 
117
                   oldtmpf.name,
166
118
                   '--label', new_filename,
167
 
                   new_abspath,
168
 
                   '--binary',
169
 
                  ]
 
119
                   newtmpf.name]
170
120
 
171
121
        # diff only allows one style to be specified; they don't override.
172
122
        # note that some of these take optargs, and the optargs can be
192
142
        if diff_opts:
193
143
            diffcmd.extend(diff_opts)
194
144
 
195
 
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
196
 
        out,err = pipe.communicate()
197
 
        rc = pipe.returncode
 
145
        rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
198
146
        
199
 
        # internal_diff() adds a trailing newline, add one here for consistency
200
 
        out += '\n'
201
 
        if rc == 2:
202
 
            # 'diff' gives retcode == 2 for all sorts of errors
203
 
            # one of those is 'Binary files differ'.
204
 
            # Bad options could also be the problem.
205
 
            # 'Binary files' is not a real error, so we suppress that error.
206
 
            lang_c_out = out
207
 
 
208
 
            # Since we got here, we want to make sure to give an i18n error
209
 
            pipe = _spawn_external_diff(diffcmd, capture_errors=False)
210
 
            out, err = pipe.communicate()
211
 
 
212
 
            # Write out the new i18n diff response
213
 
            to_file.write(out+'\n')
214
 
            if pipe.returncode != 2:
215
 
                raise errors.BzrError(
216
 
                               'external diff failed with exit code 2'
217
 
                               ' when run with LANG=C and LC_ALL=C,'
218
 
                               ' but not when run natively: %r' % (diffcmd,))
219
 
 
220
 
            first_line = lang_c_out.split('\n', 1)[0]
221
 
            # Starting with diffutils 2.8.4 the word "binary" was dropped.
222
 
            m = re.match('^(binary )?files.*differ$', first_line, re.I)
223
 
            if m is None:
224
 
                raise errors.BzrError('external diff failed with exit code 2;'
225
 
                                      ' command: %r' % (diffcmd,))
226
 
            else:
227
 
                # Binary files differ, just return
228
 
                return
229
 
 
230
 
        # If we got to here, we haven't written out the output of diff
231
 
        # do so now
232
 
        to_file.write(out)
233
 
        if rc not in (0, 1):
 
147
        if rc != 0 and rc != 1:
234
148
            # returns 1 if files differ; that's OK
235
149
            if rc < 0:
236
150
                msg = 'signal %d' % (-rc)
237
151
            else:
238
152
                msg = 'exit code %d' % rc
239
153
                
240
 
            raise errors.BzrError('external diff failed with %s; command: %r' 
241
 
                                  % (rc, diffcmd))
242
 
 
243
 
 
 
154
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
244
155
    finally:
245
156
        oldtmpf.close()                 # and delete
246
157
        newtmpf.close()
247
 
        # Clean up. Warn in case the files couldn't be deleted
248
 
        # (in case windows still holds the file open, but not
249
 
        # if the files have already been deleted)
250
 
        try:
251
 
            os.remove(old_abspath)
252
 
        except OSError, e:
253
 
            if e.errno not in (errno.ENOENT,):
254
 
                warning('Failed to delete temporary file: %s %s',
255
 
                        old_abspath, e)
256
 
        try:
257
 
            os.remove(new_abspath)
258
 
        except OSError:
259
 
            if e.errno not in (errno.ENOENT,):
260
 
                warning('Failed to delete temporary file: %s %s',
261
 
                        new_abspath, e)
262
158
 
263
159
 
264
160
@deprecated_function(zero_eight)
278
174
    supplies any two trees.
279
175
    """
280
176
    if output is None:
 
177
        import sys
281
178
        output = sys.stdout
282
179
 
283
180
    if from_spec is None:
301
198
 
302
199
def diff_cmd_helper(tree, specific_files, external_diff_options, 
303
200
                    old_revision_spec=None, new_revision_spec=None,
304
 
                    revision_specs=None,
305
201
                    old_label='a/', new_label='b/'):
306
202
    """Helper for cmd_diff.
307
203
 
308
 
    :param tree:
 
204
   tree 
309
205
        A WorkingTree
310
206
 
311
 
    :param specific_files:
 
207
    specific_files
312
208
        The specific files to compare, or None
313
209
 
314
 
    :param external_diff_options:
 
210
    external_diff_options
315
211
        If non-None, run an external diff, and pass it these options
316
212
 
317
 
    :param old_revision_spec:
 
213
    old_revision_spec
318
214
        If None, use basis tree as old revision, otherwise use the tree for
319
215
        the specified revision. 
320
216
 
321
 
    :param new_revision_spec:
 
217
    new_revision_spec
322
218
        If None, use working tree as new revision, otherwise use the tree for
323
219
        the specified revision.
324
220
    
325
 
    :param revision_specs: 
326
 
        Zero, one or two RevisionSpecs from the command line, saying what revisions 
327
 
        to compare.  This can be passed as an alternative to the old_revision_spec 
328
 
        and new_revision_spec parameters.
329
 
 
330
221
    The more general form is show_diff_trees(), where the caller
331
222
    supplies any two trees.
332
223
    """
333
 
 
334
 
    # TODO: perhaps remove the old parameters old_revision_spec and
335
 
    # new_revision_spec, since this is only really for use from cmd_diff and
336
 
    # it now always passes through a sequence of revision_specs -- mbp
337
 
    # 20061221
338
 
 
 
224
    import sys
 
225
    output = sys.stdout
339
226
    def spec_tree(spec):
340
 
        if tree:
341
 
            revision = spec.in_store(tree.branch)
342
 
        else:
343
 
            revision = spec.in_store(None)
344
 
        revision_id = revision.rev_id
345
 
        branch = revision.branch
346
 
        return branch.repository.revision_tree(revision_id)
347
 
 
348
 
    if revision_specs is not None:
349
 
        assert (old_revision_spec is None
350
 
                and new_revision_spec is None)
351
 
        if len(revision_specs) > 0:
352
 
            old_revision_spec = revision_specs[0]
353
 
        if len(revision_specs) > 1:
354
 
            new_revision_spec = revision_specs[1]
355
 
 
 
227
        revision_id = spec.in_store(tree.branch).rev_id
 
228
        return tree.branch.repository.revision_tree(revision_id)
356
229
    if old_revision_spec is None:
357
230
        old_tree = tree.basis_tree()
358
231
    else:
359
232
        old_tree = spec_tree(old_revision_spec)
360
233
 
361
 
    if (new_revision_spec is None
362
 
        or new_revision_spec.spec is None):
 
234
    if new_revision_spec is None:
363
235
        new_tree = tree
364
236
    else:
365
237
        new_tree = spec_tree(new_revision_spec)
366
238
 
367
 
    if new_tree is not tree:
368
 
        extra_trees = (tree,)
369
 
    else:
370
 
        extra_trees = None
371
 
 
372
239
    return show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
373
240
                           external_diff_options,
374
 
                           old_label=old_label, new_label=new_label,
375
 
                           extra_trees=extra_trees)
 
241
                           old_label=old_label, new_label=new_label)
376
242
 
377
243
 
378
244
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
379
245
                    external_diff_options=None,
380
 
                    old_label='a/', new_label='b/',
381
 
                    extra_trees=None):
 
246
                    old_label='a/', new_label='b/'):
382
247
    """Show in text form the changes from one tree to another.
383
248
 
384
249
    to_files
386
251
 
387
252
    external_diff_options
388
253
        If set, use an external GNU diff and pass these options.
389
 
 
390
 
    extra_trees
391
 
        If set, more Trees to use for looking up file ids
392
254
    """
393
255
    old_tree.lock_read()
394
256
    try:
395
 
        if extra_trees is not None:
396
 
            for tree in extra_trees:
397
 
                tree.lock_read()
398
257
        new_tree.lock_read()
399
258
        try:
400
259
            return _show_diff_trees(old_tree, new_tree, to_file,
401
260
                                    specific_files, external_diff_options,
402
 
                                    old_label=old_label, new_label=new_label,
403
 
                                    extra_trees=extra_trees)
 
261
                                    old_label=old_label, new_label=new_label)
404
262
        finally:
405
263
            new_tree.unlock()
406
 
            if extra_trees is not None:
407
 
                for tree in extra_trees:
408
 
                    tree.unlock()
409
264
    finally:
410
265
        old_tree.unlock()
411
266
 
412
267
 
413
268
def _show_diff_trees(old_tree, new_tree, to_file,
414
269
                     specific_files, external_diff_options, 
415
 
                     old_label='a/', new_label='b/', extra_trees=None):
 
270
                     old_label='a/', new_label='b/' ):
416
271
 
417
272
    # GNU Patch uses the epoch date to detect files that are being added
418
273
    # or removed in a diff.
421
276
    # TODO: Generation of pseudo-diffs for added/deleted files could
422
277
    # be usefully made into a much faster special case.
423
278
 
 
279
    _raise_if_doubly_unversioned(specific_files, old_tree, new_tree)
 
280
 
424
281
    if external_diff_options:
425
282
        assert isinstance(external_diff_options, basestring)
426
283
        opts = external_diff_options.split()
429
286
    else:
430
287
        diff_file = internal_diff
431
288
    
432
 
    delta = new_tree.changes_from(old_tree,
433
 
        specific_files=specific_files,
434
 
        extra_trees=extra_trees, require_versioned=True)
 
289
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
 
290
                          specific_files=specific_files)
435
291
 
436
292
    has_changes = 0
437
293
    for path, file_id, kind in delta.removed:
471
327
        has_changes = 1
472
328
        prop_str = get_prop_change(meta_modified)
473
329
        print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
474
 
        # The file may be in a different location in the old tree (because
475
 
        # the containing dir was renamed, but the file itself was not)
476
 
        old_path = old_tree.id2path(file_id)
477
 
        old_name = '%s%s\t%s' % (old_label, old_path,
478
 
                                 _patch_header_date(old_tree, file_id, old_path))
 
330
        old_name = '%s%s\t%s' % (old_label, path,
 
331
                                 _patch_header_date(old_tree, file_id, path))
479
332
        new_name = '%s%s\t%s' % (new_label, path,
480
333
                                 _patch_header_date(new_tree, file_id, path))
481
334
        if text_modified:
488
341
 
489
342
def _patch_header_date(tree, file_id, path):
490
343
    """Returns a timestamp suitable for use in a patch header."""
491
 
    mtime = tree.get_file_mtime(file_id, path)
492
 
    assert mtime is not None, \
493
 
        "got an mtime of None for file-id %s, path %s in tree %s" % (
494
 
                file_id, path, tree)
495
 
    return timestamp.format_patch_date(mtime)
496
 
 
 
344
    tm = time.gmtime(tree.get_file_mtime(file_id, path))
 
345
    return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
 
346
 
 
347
 
 
348
def _raise_if_doubly_unversioned(specific_files, old_tree, new_tree):
 
349
    """Complain if paths are not versioned in either tree."""
 
350
    if not specific_files:
 
351
        return
 
352
    old_unversioned = old_tree.filter_unversioned_files(specific_files)
 
353
    new_unversioned = new_tree.filter_unversioned_files(specific_files)
 
354
    unversioned = old_unversioned.intersection(new_unversioned)
 
355
    if unversioned:
 
356
        raise errors.PathsNotVersionedError(sorted(unversioned))
 
357
    
497
358
 
498
359
def _raise_if_nonexistent(paths, old_tree, new_tree):
499
360
    """Complain if paths are not in either inventory or tree.