~bzr-pqm/bzr/bzr.dev

1 by mbp at sourcefrog
import from baz patch-364
1
#! /usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18
from trace import mutter
356 by Martin Pool
- pychecker fixes in bzrlib.diff
19
from errors import BzrError
1 by mbp at sourcefrog
import from baz patch-364
20
21
767 by Martin Pool
- files are only reported as modified if their name or parent has changed,
22
# TODO: Rather than building a changeset object, we should probably
23
# invoke callbacks on an object.  That object can either accumulate a
24
# list, write them out directly, etc etc.
25
568 by Martin Pool
- start adding support for showing diffs by calling out to
26
def internal_diff(old_label, oldlines, new_label, newlines, to_file):
475 by Martin Pool
- rewrite diff using compare_trees()
27
    import difflib
28
    
29
    # FIXME: difflib is wrong if there is no trailing newline.
30
    # The syntax used by patch seems to be "\ No newline at
31
    # end of file" following the last diff line from that
32
    # file.  This is not trivial to insert into the
33
    # unified_diff output and it might be better to just fix
34
    # or replace that function.
35
36
    # In the meantime we at least make sure the patch isn't
37
    # mangled.
38
39
40
    # Special workaround for Python2.3, where difflib fails if
41
    # both sequences are empty.
42
    if not oldlines and not newlines:
43
        return
44
45
    nonl = False
46
47
    if oldlines and (oldlines[-1][-1] != '\n'):
48
        oldlines[-1] += '\n'
49
        nonl = True
50
    if newlines and (newlines[-1][-1] != '\n'):
51
        newlines[-1] += '\n'
52
        nonl = True
53
568 by Martin Pool
- start adding support for showing diffs by calling out to
54
    ud = difflib.unified_diff(oldlines, newlines,
55
                              fromfile=old_label, tofile=new_label)
475 by Martin Pool
- rewrite diff using compare_trees()
56
57
    # work-around for difflib being too smart for its own good
58
    # if /dev/null is "1,0", patch won't recognize it as /dev/null
59
    if not oldlines:
60
        ud = list(ud)
61
        ud[2] = ud[2].replace('-1,0', '-0,0')
62
    elif not newlines:
63
        ud = list(ud)
64
        ud[2] = ud[2].replace('+1,0', '+0,0')
65
804 by Martin Pool
Patch from John:
66
    for line in ud:
67
        to_file.write(line)
475 by Martin Pool
- rewrite diff using compare_trees()
68
    if nonl:
69
        print >>to_file, "\\ No newline at end of file"
70
    print >>to_file
71
72
550 by Martin Pool
- Refactor diff code into one that works purely on
73
568 by Martin Pool
- start adding support for showing diffs by calling out to
74
571 by Martin Pool
- new --diff-options to pass options through to external
75
def external_diff(old_label, oldlines, new_label, newlines, to_file,
76
                  diff_opts):
568 by Martin Pool
- start adding support for showing diffs by calling out to
77
    """Display a diff by calling out to the external diff program."""
78
    import sys
79
    
80
    if to_file != sys.stdout:
81
        raise NotImplementedError("sorry, can't send external diff other than to stdout yet",
82
                                  to_file)
83
581 by Martin Pool
- make sure any bzr output is flushed before
84
    # make sure our own output is properly ordered before the diff
85
    to_file.flush()
86
568 by Martin Pool
- start adding support for showing diffs by calling out to
87
    from tempfile import NamedTemporaryFile
571 by Martin Pool
- new --diff-options to pass options through to external
88
    import os
568 by Martin Pool
- start adding support for showing diffs by calling out to
89
90
    oldtmpf = NamedTemporaryFile()
91
    newtmpf = NamedTemporaryFile()
92
93
    try:
94
        # TODO: perhaps a special case for comparing to or from the empty
95
        # sequence; can just use /dev/null on Unix
96
97
        # TODO: if either of the files being compared already exists as a
98
        # regular named file (e.g. in the working directory) then we can
99
        # compare directly to that, rather than copying it.
100
101
        oldtmpf.writelines(oldlines)
102
        newtmpf.writelines(newlines)
103
104
        oldtmpf.flush()
105
        newtmpf.flush()
106
571 by Martin Pool
- new --diff-options to pass options through to external
107
        if not diff_opts:
108
            diff_opts = []
109
        diffcmd = ['diff',
110
                   '--label', old_label,
111
                   oldtmpf.name,
112
                   '--label', new_label,
113
                   newtmpf.name]
114
115
        # diff only allows one style to be specified; they don't override.
116
        # note that some of these take optargs, and the optargs can be
117
        # directly appended to the options.
118
        # this is only an approximate parser; it doesn't properly understand
119
        # the grammar.
120
        for s in ['-c', '-u', '-C', '-U',
121
                  '-e', '--ed',
122
                  '-q', '--brief',
123
                  '--normal',
124
                  '-n', '--rcs',
125
                  '-y', '--side-by-side',
126
                  '-D', '--ifdef']:
127
            for j in diff_opts:
128
                if j.startswith(s):
129
                    break
130
            else:
131
                continue
132
            break
133
        else:
134
            diffcmd.append('-u')
135
                  
136
        if diff_opts:
137
            diffcmd.extend(diff_opts)
138
139
        rc = os.spawnvp(os.P_WAIT, 'diff', diffcmd)
140
        
141
        if rc != 0 and rc != 1:
142
            # returns 1 if files differ; that's OK
143
            if rc < 0:
144
                msg = 'signal %d' % (-rc)
145
            else:
146
                msg = 'exit code %d' % rc
147
                
148
            raise BzrError('external diff failed with %s; command: %r' % (rc, diffcmd))
568 by Martin Pool
- start adding support for showing diffs by calling out to
149
    finally:
150
        oldtmpf.close()                 # and delete
151
        newtmpf.close()
152
    
153
154
571 by Martin Pool
- new --diff-options to pass options through to external
155
def show_diff(b, revision, specific_files, external_diff_options=None):
619 by Martin Pool
doc
156
    """Shortcut for showing the diff to the working tree.
157
158
    b
159
        Branch.
160
161
    revision
162
        None for each, or otherwise the old revision to compare against.
163
    
164
    The more general form is show_diff_trees(), where the caller
165
    supplies any two trees.
166
    """
475 by Martin Pool
- rewrite diff using compare_trees()
167
    import sys
168
329 by Martin Pool
- refactor command functions into command classes
169
    if revision == None:
170
        old_tree = b.basis_tree()
171
    else:
172
        old_tree = b.revision_tree(b.lookup_revision(revision))
173
        
174
    new_tree = b.working_tree()
175
571 by Martin Pool
- new --diff-options to pass options through to external
176
    show_diff_trees(old_tree, new_tree, sys.stdout, specific_files,
177
                    external_diff_options)
178
179
180
181
def show_diff_trees(old_tree, new_tree, to_file, specific_files=None,
182
                    external_diff_options=None):
550 by Martin Pool
- Refactor diff code into one that works purely on
183
    """Show in text form the changes from one tree to another.
184
185
    to_files
186
        If set, include only changes to these files.
571 by Martin Pool
- new --diff-options to pass options through to external
187
188
    external_diff_options
189
        If set, use an external GNU diff and pass these options.
550 by Martin Pool
- Refactor diff code into one that works purely on
190
    """
191
329 by Martin Pool
- refactor command functions into command classes
192
    # TODO: Options to control putting on a prefix or suffix, perhaps as a format string
193
    old_label = ''
194
    new_label = ''
195
196
    DEVNULL = '/dev/null'
197
    # Windows users, don't panic about this filename -- it is a
198
    # special signal to GNU patch that the file should be created or
199
    # deleted respectively.
200
201
    # TODO: Generation of pseudo-diffs for added/deleted files could
202
    # be usefully made into a much faster special case.
203
571 by Martin Pool
- new --diff-options to pass options through to external
204
    if external_diff_options:
205
        assert isinstance(external_diff_options, basestring)
206
        opts = external_diff_options.split()
207
        def diff_file(olab, olines, nlab, nlines, to_file):
208
            external_diff(olab, olines, nlab, nlines, to_file, opts)
209
    else:
210
        diff_file = internal_diff
211
    
212
478 by Martin Pool
- put back support for running diff or status on
213
    delta = compare_trees(old_tree, new_tree, want_unchanged=False,
483 by Martin Pool
- change 'file_list' to more explanatory 'specific_files'
214
                          specific_files=specific_files)
475 by Martin Pool
- rewrite diff using compare_trees()
215
216
    for path, file_id, kind in delta.removed:
643 by Martin Pool
- fix redirection of messages to file in diff
217
        print >>to_file, '*** removed %s %r' % (kind, path)
475 by Martin Pool
- rewrite diff using compare_trees()
218
        if kind == 'file':
568 by Martin Pool
- start adding support for showing diffs by calling out to
219
            diff_file(old_label + path,
220
                      old_tree.get_file(file_id).readlines(),
221
                      DEVNULL, 
222
                      [],
223
                      to_file)
475 by Martin Pool
- rewrite diff using compare_trees()
224
225
    for path, file_id, kind in delta.added:
643 by Martin Pool
- fix redirection of messages to file in diff
226
        print >>to_file, '*** added %s %r' % (kind, path)
475 by Martin Pool
- rewrite diff using compare_trees()
227
        if kind == 'file':
568 by Martin Pool
- start adding support for showing diffs by calling out to
228
            diff_file(DEVNULL,
229
                      [],
230
                      new_label + path,
231
                      new_tree.get_file(file_id).readlines(),
232
                      to_file)
475 by Martin Pool
- rewrite diff using compare_trees()
233
234
    for old_path, new_path, file_id, kind, text_modified in delta.renamed:
643 by Martin Pool
- fix redirection of messages to file in diff
235
        print >>to_file, '*** renamed %s %r => %r' % (kind, old_path, new_path)
475 by Martin Pool
- rewrite diff using compare_trees()
236
        if text_modified:
568 by Martin Pool
- start adding support for showing diffs by calling out to
237
            diff_file(old_label + old_path,
238
                      old_tree.get_file(file_id).readlines(),
239
                      new_label + new_path,
240
                      new_tree.get_file(file_id).readlines(),
241
                      to_file)
475 by Martin Pool
- rewrite diff using compare_trees()
242
243
    for path, file_id, kind in delta.modified:
643 by Martin Pool
- fix redirection of messages to file in diff
244
        print >>to_file, '*** modified %s %r' % (kind, path)
475 by Martin Pool
- rewrite diff using compare_trees()
245
        if kind == 'file':
568 by Martin Pool
- start adding support for showing diffs by calling out to
246
            diff_file(old_label + path,
247
                      old_tree.get_file(file_id).readlines(),
248
                      new_label + path,
249
                      new_tree.get_file(file_id).readlines(),
250
                      to_file)
329 by Martin Pool
- refactor command functions into command classes
251
252
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
253
558 by Martin Pool
- All top-level classes inherit from object
254
class TreeDelta(object):
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
255
    """Describes changes from one tree to another.
256
257
    Contains four lists:
258
259
    added
475 by Martin Pool
- rewrite diff using compare_trees()
260
        (path, id, kind)
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
261
    removed
475 by Martin Pool
- rewrite diff using compare_trees()
262
        (path, id, kind)
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
263
    renamed
475 by Martin Pool
- rewrite diff using compare_trees()
264
        (oldpath, newpath, id, kind, text_modified)
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
265
    modified
475 by Martin Pool
- rewrite diff using compare_trees()
266
        (path, id, kind)
463 by Martin Pool
- compare_trees() also reports unchanged files
267
    unchanged
475 by Martin Pool
- rewrite diff using compare_trees()
268
        (path, id, kind)
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
269
460 by Martin Pool
- new testing command compare-trees
270
    Each id is listed only once.
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
271
460 by Martin Pool
- new testing command compare-trees
272
    Files that are both modified and renamed are listed only in
273
    renamed, with the text_modified flag true.
463 by Martin Pool
- compare_trees() also reports unchanged files
274
767 by Martin Pool
- files are only reported as modified if their name or parent has changed,
275
    Files are only considered renamed if their name has changed or
276
    their parent directory has changed.  Renaming a directory
277
    does not count as renaming all its contents.
278
463 by Martin Pool
- compare_trees() also reports unchanged files
279
    The lists are normally sorted when the delta is created.
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
280
    """
281
    def __init__(self):
282
        self.added = []
283
        self.removed = []
284
        self.renamed = []
285
        self.modified = []
463 by Martin Pool
- compare_trees() also reports unchanged files
286
        self.unchanged = []
379 by Martin Pool
- Simpler compare_inventories() to possibly replace diff_trees
287
747 by Martin Pool
- TreeDelta __eq__ and __ne__ methods
288
    def __eq__(self, other):
289
        if not isinstance(other, TreeDelta):
290
            return False
291
        return self.added == other.added \
292
               and self.removed == other.removed \
293
               and self.renamed == other.renamed \
294
               and self.modified == other.modified \
295
               and self.unchanged == other.unchanged
296
297
    def __ne__(self, other):
298
        return not (self == other)
299
639 by Martin Pool
- add TreeDelta repr
300
    def __repr__(self):
301
        return "TreeDelta(added=%r, removed=%r, renamed=%r, modified=%r," \
302
            " unchanged=%r)" % (self.added, self.removed, self.renamed,
303
            self.modified, self.unchanged)
304
622 by Martin Pool
Updated merge patch from Aaron
305
    def has_changed(self):
306
        changes = len(self.added) + len(self.removed) + len(self.renamed)
307
        changes += len(self.modified) 
308
        return (changes != 0)
531 by Martin Pool
- new utility TreeDelta.touches_file_id
309
310
    def touches_file_id(self, file_id):
311
        """Return True if file_id is modified by this delta."""
312
        for l in self.added, self.removed, self.modified:
313
            for v in l:
314
                if v[1] == file_id:
315
                    return True
316
        for v in self.renamed:
317
            if v[2] == file_id:
318
                return True
319
        return False
320
            
321
465 by Martin Pool
- Move show_status() out of Branch into a new function in
322
    def show(self, to_file, show_ids=False, show_unchanged=False):
323
        def show_list(files):
475 by Martin Pool
- rewrite diff using compare_trees()
324
            for path, fid, kind in files:
325
                if kind == 'directory':
326
                    path += '/'
327
                elif kind == 'symlink':
328
                    path += '@'
329
                    
465 by Martin Pool
- Move show_status() out of Branch into a new function in
330
                if show_ids:
331
                    print >>to_file, '  %-30s %s' % (path, fid)
332
                else:
333
                    print >>to_file, ' ', path
334
            
460 by Martin Pool
- new testing command compare-trees
335
        if self.removed:
475 by Martin Pool
- rewrite diff using compare_trees()
336
            print >>to_file, 'removed:'
465 by Martin Pool
- Move show_status() out of Branch into a new function in
337
            show_list(self.removed)
338
                
460 by Martin Pool
- new testing command compare-trees
339
        if self.added:
475 by Martin Pool
- rewrite diff using compare_trees()
340
            print >>to_file, 'added:'
465 by Martin Pool
- Move show_status() out of Branch into a new function in
341
            show_list(self.added)
342
460 by Martin Pool
- new testing command compare-trees
343
        if self.renamed:
475 by Martin Pool
- rewrite diff using compare_trees()
344
            print >>to_file, 'renamed:'
345
            for oldpath, newpath, fid, kind, text_modified in self.renamed:
460 by Martin Pool
- new testing command compare-trees
346
                if show_ids:
347
                    print >>to_file, '  %s => %s %s' % (oldpath, newpath, fid)
348
                else:
349
                    print >>to_file, '  %s => %s' % (oldpath, newpath)
465 by Martin Pool
- Move show_status() out of Branch into a new function in
350
                    
460 by Martin Pool
- new testing command compare-trees
351
        if self.modified:
475 by Martin Pool
- rewrite diff using compare_trees()
352
            print >>to_file, 'modified:'
465 by Martin Pool
- Move show_status() out of Branch into a new function in
353
            show_list(self.modified)
354
            
355
        if show_unchanged and self.unchanged:
475 by Martin Pool
- rewrite diff using compare_trees()
356
            print >>to_file, 'unchanged:'
465 by Martin Pool
- Move show_status() out of Branch into a new function in
357
            show_list(self.unchanged)
460 by Martin Pool
- new testing command compare-trees
358
359
360
746 by Martin Pool
- compare_trees doesn't return unchanged files by default
361
def compare_trees(old_tree, new_tree, want_unchanged=False, specific_files=None):
478 by Martin Pool
- put back support for running diff or status on
362
    """Describe changes from one tree to another.
363
364
    Returns a TreeDelta with details of added, modified, renamed, and
365
    deleted entries.
366
367
    The root entry is specifically exempt.
368
369
    This only considers versioned files.
370
371
    want_unchanged
485 by Martin Pool
- move commit code into its own module
372
        If true, also list files unchanged from one version to
373
        the next.
478 by Martin Pool
- put back support for running diff or status on
374
483 by Martin Pool
- change 'file_list' to more explanatory 'specific_files'
375
    specific_files
485 by Martin Pool
- move commit code into its own module
376
        If true, only check for changes to specified names or
377
        files within them.
478 by Martin Pool
- put back support for running diff or status on
378
    """
485 by Martin Pool
- move commit code into its own module
379
380
    from osutils import is_inside_any
381
    
460 by Martin Pool
- new testing command compare-trees
382
    old_inv = old_tree.inventory
383
    new_inv = new_tree.inventory
384
    delta = TreeDelta()
475 by Martin Pool
- rewrite diff using compare_trees()
385
    mutter('start compare_trees')
478 by Martin Pool
- put back support for running diff or status on
386
485 by Martin Pool
- move commit code into its own module
387
    # TODO: match for specific files can be rather smarter by finding
388
    # the IDs of those files up front and then considering only that.
478 by Martin Pool
- put back support for running diff or status on
389
462 by Martin Pool
- New form 'file_id in tree' to check if the file is present
390
    for file_id in old_tree:
391
        if file_id in new_tree:
460 by Martin Pool
- new testing command compare-trees
392
            kind = old_inv.get_file_kind(file_id)
475 by Martin Pool
- rewrite diff using compare_trees()
393
            assert kind == new_inv.get_file_kind(file_id)
394
            
460 by Martin Pool
- new testing command compare-trees
395
            assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
396
                   'invalid file kind %r' % kind
477 by Martin Pool
- fix header for listing of unknown files
397
398
            if kind == 'root_directory':
399
                continue
400
            
401
            old_path = old_inv.id2path(file_id)
402
            new_path = new_inv.id2path(file_id)
403
767 by Martin Pool
- files are only reported as modified if their name or parent has changed,
404
            old_ie = old_inv[file_id]
405
            new_ie = new_inv[file_id]
406
483 by Martin Pool
- change 'file_list' to more explanatory 'specific_files'
407
            if specific_files:
485 by Martin Pool
- move commit code into its own module
408
                if (not is_inside_any(specific_files, old_path) 
409
                    and not is_inside_any(specific_files, new_path)):
478 by Martin Pool
- put back support for running diff or status on
410
                    continue
411
460 by Martin Pool
- new testing command compare-trees
412
            if kind == 'file':
413
                old_sha1 = old_tree.get_file_sha1(file_id)
414
                new_sha1 = new_tree.get_file_sha1(file_id)
415
                text_modified = (old_sha1 != new_sha1)
416
            else:
417
                ## mutter("no text to check for %r %r" % (file_id, kind))
418
                text_modified = False
471 by Martin Pool
- actually avoid reporting unchanged files if not required
419
420
            # TODO: Can possibly avoid calculating path strings if the
421
            # two files are unchanged and their names and parents are
422
            # the same and the parents are unchanged all the way up.
423
            # May not be worthwhile.
460 by Martin Pool
- new testing command compare-trees
424
            
767 by Martin Pool
- files are only reported as modified if their name or parent has changed,
425
            if (old_ie.name != new_ie.name
426
                or old_ie.parent_id != new_ie.parent_id):
475 by Martin Pool
- rewrite diff using compare_trees()
427
                delta.renamed.append((old_path, new_path, file_id, kind,
428
                                      text_modified))
460 by Martin Pool
- new testing command compare-trees
429
            elif text_modified:
475 by Martin Pool
- rewrite diff using compare_trees()
430
                delta.modified.append((new_path, file_id, kind))
471 by Martin Pool
- actually avoid reporting unchanged files if not required
431
            elif want_unchanged:
475 by Martin Pool
- rewrite diff using compare_trees()
432
                delta.unchanged.append((new_path, file_id, kind))
460 by Martin Pool
- new testing command compare-trees
433
        else:
566 by Martin Pool
- fix bug in reporting diffs between trees where files have
434
            kind = old_inv.get_file_kind(file_id)
485 by Martin Pool
- move commit code into its own module
435
            old_path = old_inv.id2path(file_id)
436
            if specific_files:
437
                if not is_inside_any(specific_files, old_path):
438
                    continue
439
            delta.removed.append((old_path, file_id, kind))
475 by Martin Pool
- rewrite diff using compare_trees()
440
441
    mutter('start looking for new files')
460 by Martin Pool
- new testing command compare-trees
442
    for file_id in new_inv:
443
        if file_id in old_inv:
444
            continue
478 by Martin Pool
- put back support for running diff or status on
445
        new_path = new_inv.id2path(file_id)
483 by Martin Pool
- change 'file_list' to more explanatory 'specific_files'
446
        if specific_files:
485 by Martin Pool
- move commit code into its own module
447
            if not is_inside_any(specific_files, new_path):
478 by Martin Pool
- put back support for running diff or status on
448
                continue
475 by Martin Pool
- rewrite diff using compare_trees()
449
        kind = new_inv.get_file_kind(file_id)
478 by Martin Pool
- put back support for running diff or status on
450
        delta.added.append((new_path, file_id, kind))
460 by Martin Pool
- new testing command compare-trees
451
            
452
    delta.removed.sort()
453
    delta.added.sort()
454
    delta.renamed.sort()
455
    delta.modified.sort()
474 by Martin Pool
- sort unchanged files
456
    delta.unchanged.sort()
460 by Martin Pool
- new testing command compare-trees
457
458
    return delta