~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/diff.py

Merged bzr.dev and updated NEWS with a better description of changes

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
 
import errno
18
17
import os
19
18
import re
 
19
import sys
 
20
 
 
21
from bzrlib.lazy_import import lazy_import
 
22
lazy_import(globals(), """
 
23
import errno
20
24
import subprocess
21
 
import sys
22
25
import tempfile
23
26
import time
24
27
 
 
28
from bzrlib import (
 
29
    errors,
 
30
    osutils,
 
31
    patiencediff,
 
32
    textfile,
 
33
    timestamp,
 
34
    )
 
35
""")
 
36
 
25
37
# compatability - plugins import compare_trees from diff!!!
26
38
# deprecated as of 0.10
27
39
from bzrlib.delta import compare_trees
28
 
from bzrlib.errors import BzrError
29
 
import bzrlib.errors as errors
30
 
import bzrlib.osutils
31
 
from bzrlib.patiencediff import unified_diff
32
 
import bzrlib.patiencediff
33
 
from bzrlib.symbol_versioning import (deprecated_function,
34
 
        zero_eight)
35
 
from bzrlib.textfile import check_text_lines
 
40
from bzrlib.symbol_versioning import (
 
41
        deprecated_function,
 
42
        zero_eight,
 
43
        )
36
44
from bzrlib.trace import mutter, warning
37
45
 
38
46
 
60
68
        return
61
69
    
62
70
    if allow_binary is False:
63
 
        check_text_lines(oldlines)
64
 
        check_text_lines(newlines)
 
71
        textfile.check_text_lines(oldlines)
 
72
        textfile.check_text_lines(newlines)
65
73
 
66
74
    if sequence_matcher is None:
67
 
        sequence_matcher = bzrlib.patiencediff.PatienceSequenceMatcher
68
 
    ud = unified_diff(oldlines, newlines,
 
75
        sequence_matcher = patiencediff.PatienceSequenceMatcher
 
76
    ud = patiencediff.unified_diff(oldlines, newlines,
69
77
                      fromfile=old_filename.encode(path_encoding),
70
78
                      tofile=new_filename.encode(path_encoding),
71
79
                      sequencematcher=sequence_matcher)
88
96
    print >>to_file
89
97
 
90
98
 
 
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
 
91
136
def external_diff(old_filename, oldlines, new_filename, newlines, to_file,
92
137
                  diff_opts):
93
138
    """Display a diff by calling out to the external diff program."""
147
192
        if diff_opts:
148
193
            diffcmd.extend(diff_opts)
149
194
 
150
 
        try:
151
 
            pipe = subprocess.Popen(diffcmd,
152
 
                                    stdin=subprocess.PIPE,
153
 
                                    stdout=subprocess.PIPE)
154
 
        except OSError, e:
155
 
            if e.errno == errno.ENOENT:
156
 
                raise errors.NoDiff(str(e))
157
 
            raise
158
 
        pipe.stdin.close()
159
 
 
160
 
        first_line = pipe.stdout.readline()
161
 
        to_file.write(first_line)
162
 
        bzrlib.osutils.pumpfile(pipe.stdout, to_file)
163
 
        rc = pipe.wait()
 
195
        pipe = _spawn_external_diff(diffcmd, capture_errors=True)
 
196
        out,err = pipe.communicate()
 
197
        rc = pipe.returncode
164
198
        
 
199
        # internal_diff() adds a trailing newline, add one here for consistency
 
200
        out += '\n'
165
201
        if rc == 2:
166
202
            # 'diff' gives retcode == 2 for all sorts of errors
167
203
            # one of those is 'Binary files differ'.
168
204
            # Bad options could also be the problem.
169
 
            # 'Binary files' is not a real error, so we suppress that error
170
 
            m = re.match('^binary files.*differ$', first_line, re.I)
171
 
            if not m:
172
 
                raise BzrError('external diff failed with exit code 2;'
173
 
                               ' command: %r' % (diffcmd,))
174
 
        elif rc not in (0, 1):
 
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):
175
234
            # returns 1 if files differ; that's OK
176
235
            if rc < 0:
177
236
                msg = 'signal %d' % (-rc)
178
237
            else:
179
238
                msg = 'exit code %d' % rc
180
239
                
181
 
            raise BzrError('external diff failed with %s; command: %r' 
182
 
                           % (rc, diffcmd))
 
240
            raise errors.BzrError('external diff failed with %s; command: %r' 
 
241
                                  % (rc, diffcmd))
183
242
 
184
 
        # internal_diff() adds a trailing newline, add one here for consistency
185
 
        to_file.write('\n')
186
243
 
187
244
    finally:
188
245
        oldtmpf.close()                 # and delete
244
301
 
245
302
def diff_cmd_helper(tree, specific_files, external_diff_options, 
246
303
                    old_revision_spec=None, new_revision_spec=None,
 
304
                    revision_specs=None,
247
305
                    old_label='a/', new_label='b/'):
248
306
    """Helper for cmd_diff.
249
307
 
250
 
   tree 
 
308
    :param tree:
251
309
        A WorkingTree
252
310
 
253
 
    specific_files
 
311
    :param specific_files:
254
312
        The specific files to compare, or None
255
313
 
256
 
    external_diff_options
 
314
    :param external_diff_options:
257
315
        If non-None, run an external diff, and pass it these options
258
316
 
259
 
    old_revision_spec
 
317
    :param old_revision_spec:
260
318
        If None, use basis tree as old revision, otherwise use the tree for
261
319
        the specified revision. 
262
320
 
263
 
    new_revision_spec
 
321
    :param new_revision_spec:
264
322
        If None, use working tree as new revision, otherwise use the tree for
265
323
        the specified revision.
266
324
    
 
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
 
267
330
    The more general form is show_diff_trees(), where the caller
268
331
    supplies any two trees.
269
332
    """
 
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
 
270
339
    def spec_tree(spec):
271
340
        if tree:
272
341
            revision = spec.in_store(tree.branch)
275
344
        revision_id = revision.rev_id
276
345
        branch = revision.branch
277
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
 
278
356
    if old_revision_spec is None:
279
357
        old_tree = tree.basis_tree()
280
358
    else:
281
359
        old_tree = spec_tree(old_revision_spec)
282
360
 
283
 
    if new_revision_spec is None:
 
361
    if (new_revision_spec is None
 
362
        or new_revision_spec.spec is None):
284
363
        new_tree = tree
285
364
    else:
286
365
        new_tree = spec_tree(new_revision_spec)
 
366
 
287
367
    if new_tree is not tree:
288
368
        extra_trees = (tree,)
289
369
    else:
312
392
    """
313
393
    old_tree.lock_read()
314
394
    try:
 
395
        if extra_trees is not None:
 
396
            for tree in extra_trees:
 
397
                tree.lock_read()
315
398
        new_tree.lock_read()
316
399
        try:
317
400
            return _show_diff_trees(old_tree, new_tree, to_file,
320
403
                                    extra_trees=extra_trees)
321
404
        finally:
322
405
            new_tree.unlock()
 
406
            if extra_trees is not None:
 
407
                for tree in extra_trees:
 
408
                    tree.unlock()
323
409
    finally:
324
410
        old_tree.unlock()
325
411
 
350
436
    has_changes = 0
351
437
    for path, file_id, kind in delta.removed:
352
438
        has_changes = 1
353
 
        print >>to_file, '=== removed %s %r' % (kind, path.encode('utf8'))
 
439
        print >>to_file, "=== removed %s '%s'" % (kind, path.encode('utf8'))
354
440
        old_name = '%s%s\t%s' % (old_label, path,
355
441
                                 _patch_header_date(old_tree, file_id, path))
356
442
        new_name = '%s%s\t%s' % (new_label, path, EPOCH_DATE)
358
444
                                         new_name, None, None, to_file)
359
445
    for path, file_id, kind in delta.added:
360
446
        has_changes = 1
361
 
        print >>to_file, '=== added %s %r' % (kind, path.encode('utf8'))
 
447
        print >>to_file, "=== added %s '%s'" % (kind, path.encode('utf8'))
362
448
        old_name = '%s%s\t%s' % (old_label, path, EPOCH_DATE)
363
449
        new_name = '%s%s\t%s' % (new_label, path,
364
450
                                 _patch_header_date(new_tree, file_id, path))
369
455
         text_modified, meta_modified) in delta.renamed:
370
456
        has_changes = 1
371
457
        prop_str = get_prop_change(meta_modified)
372
 
        print >>to_file, '=== renamed %s %r => %r%s' % (
 
458
        print >>to_file, "=== renamed %s '%s' => %r%s" % (
373
459
                    kind, old_path.encode('utf8'),
374
460
                    new_path.encode('utf8'), prop_str)
375
461
        old_name = '%s%s\t%s' % (old_label, old_path,
384
470
    for path, file_id, kind, text_modified, meta_modified in delta.modified:
385
471
        has_changes = 1
386
472
        prop_str = get_prop_change(meta_modified)
387
 
        print >>to_file, '=== modified %s %r%s' % (kind, path.encode('utf8'), prop_str)
388
 
        old_name = '%s%s\t%s' % (old_label, path,
389
 
                                 _patch_header_date(old_tree, file_id, path))
 
473
        print >>to_file, "=== modified %s '%s'%s" % (kind, path.encode('utf8'),
 
474
                                                     prop_str)
 
475
        # The file may be in a different location in the old tree (because
 
476
        # the containing dir was renamed, but the file itself was not)
 
477
        old_path = old_tree.id2path(file_id)
 
478
        old_name = '%s%s\t%s' % (old_label, old_path,
 
479
                                 _patch_header_date(old_tree, file_id, old_path))
390
480
        new_name = '%s%s\t%s' % (new_label, path,
391
481
                                 _patch_header_date(new_tree, file_id, path))
392
482
        if text_modified:
399
489
 
400
490
def _patch_header_date(tree, file_id, path):
401
491
    """Returns a timestamp suitable for use in a patch header."""
402
 
    tm = time.gmtime(tree.get_file_mtime(file_id, path))
403
 
    return time.strftime('%Y-%m-%d %H:%M:%S +0000', tm)
 
492
    mtime = tree.get_file_mtime(file_id, path)
 
493
    assert mtime is not None, \
 
494
        "got an mtime of None for file-id %s, path %s in tree %s" % (
 
495
                file_id, path, tree)
 
496
    return timestamp.format_patch_date(mtime)
404
497
 
405
498
 
406
499
def _raise_if_nonexistent(paths, old_tree, new_tree):