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