/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/delta.py

  • Committer: Marius Kruger
  • Date: 2010-07-10 21:28:56 UTC
  • mto: (5384.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5385.
  • Revision ID: marius.kruger@enerweb.co.za-20100710212856-uq4ji3go0u5se7hx
* Update documentation
* add NEWS

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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
 
from __future__ import absolute_import
18
 
 
19
 
from breezy import (
 
17
from bzrlib import (
20
18
    osutils,
21
 
    trace,
22
 
    )
23
 
from .sixish import (
24
 
    StringIO,
25
 
    )
26
 
from .tree import TreeChange
 
19
    )
 
20
from bzrlib.trace import is_quiet
27
21
 
28
22
 
29
23
class TreeDelta(object):
30
24
    """Describes changes from one tree to another.
31
25
 
32
 
    Contains seven lists with TreeChange objects.
 
26
    Contains seven lists:
33
27
 
34
28
    added
 
29
        (path, id, kind)
35
30
    removed
 
31
        (path, id, kind)
36
32
    renamed
37
 
    copied
 
33
        (oldpath, newpath, id, kind, text_modified, meta_modified)
38
34
    kind_changed
 
35
        (path, id, old_kind, new_kind)
39
36
    modified
 
37
        (path, id, kind, text_modified, meta_modified)
40
38
    unchanged
 
39
        (path, id, kind)
41
40
    unversioned
 
41
        (path, None, kind)
42
42
 
43
43
    Each id is listed only once.
44
44
 
45
 
    Files that are both modified and renamed or copied are listed only in
46
 
    renamed or copied, with the text_modified flag true. The text_modified
 
45
    Files that are both modified and renamed are listed only in
 
46
    renamed, with the text_modified flag true. The text_modified
47
47
    applies either to the content of the file or the target of the
48
48
    symbolic link, depending of the kind of file.
49
49
 
53
53
 
54
54
    The lists are normally sorted when the delta is created.
55
55
    """
56
 
 
57
56
    def __init__(self):
58
57
        self.added = []
59
58
        self.removed = []
60
59
        self.renamed = []
61
 
        self.copied = []
62
60
        self.kind_changed = []
63
61
        self.modified = []
64
62
        self.unchanged = []
65
63
        self.unversioned = []
66
 
        self.missing = []
67
64
 
68
65
    def __eq__(self, other):
69
66
        if not isinstance(other, TreeDelta):
70
67
            return False
71
68
        return self.added == other.added \
72
 
            and self.removed == other.removed \
73
 
            and self.renamed == other.renamed \
74
 
            and self.copied == other.copied \
75
 
            and self.modified == other.modified \
76
 
            and self.unchanged == other.unchanged \
77
 
            and self.kind_changed == other.kind_changed \
78
 
            and self.unversioned == other.unversioned
 
69
               and self.removed == other.removed \
 
70
               and self.renamed == other.renamed \
 
71
               and self.modified == other.modified \
 
72
               and self.unchanged == other.unchanged \
 
73
               and self.kind_changed == other.kind_changed \
 
74
               and self.unversioned == other.unversioned
79
75
 
80
76
    def __ne__(self, other):
81
77
        return not (self == other)
82
78
 
83
79
    def __repr__(self):
84
80
        return "TreeDelta(added=%r, removed=%r, renamed=%r," \
85
 
            " copied=%r, kind_changed=%r, modified=%r, unchanged=%r," \
86
 
            " unversioned=%r)" % (
87
 
                self.added, self.removed, self.renamed, self.copied,
88
 
                self.kind_changed, self.modified, self.unchanged,
89
 
                self.unversioned)
 
81
            " kind_changed=%r, modified=%r, unchanged=%r," \
 
82
            " unversioned=%r)" % (self.added,
 
83
            self.removed, self.renamed, self.kind_changed, self.modified,
 
84
            self.unchanged, self.unversioned)
90
85
 
91
86
    def has_changed(self):
92
87
        return bool(self.modified
93
88
                    or self.added
94
89
                    or self.removed
95
90
                    or self.renamed
96
 
                    or self.copied
97
91
                    or self.kind_changed)
98
92
 
 
93
    def touches_file_id(self, file_id):
 
94
        """Return True if file_id is modified by this delta."""
 
95
        for l in self.added, self.removed, self.modified:
 
96
            for v in l:
 
97
                if v[1] == file_id:
 
98
                    return True
 
99
        for v in self.renamed:
 
100
            if v[2] == file_id:
 
101
                return True
 
102
        for v in self.kind_changed:
 
103
            if v[1] == file_id:
 
104
                return True
 
105
        return False
 
106
 
99
107
    def get_changes_as_text(self, show_ids=False, show_unchanged=False,
100
108
                            short_status=False):
101
 
        output = StringIO()
 
109
        import StringIO
 
110
        output = StringIO.StringIO()
102
111
        report_delta(output, self, short_status, show_ids, show_unchanged)
103
112
        return output.getvalue()
104
113
 
110
119
    delta = TreeDelta()
111
120
    # mutter('start compare_trees')
112
121
 
113
 
    for change in new_tree.iter_changes(
114
 
            old_tree, want_unchanged, specific_files, extra_trees=extra_trees,
 
122
    for (file_id, path, content_change, versioned, parent_id, name, kind,
 
123
         executable) in new_tree.iter_changes(old_tree, want_unchanged,
 
124
            specific_files, extra_trees=extra_trees,
115
125
            require_versioned=require_versioned,
116
126
            want_unversioned=want_unversioned):
117
 
        if change.versioned == (False, False):
118
 
            delta.unversioned.append(change)
119
 
            continue
120
 
        if not include_root and (None, None) == change.parent_id:
121
 
            continue
122
 
        fully_present = tuple(
123
 
            (change.versioned[x] and change.kind[x] is not None)
124
 
            for x in range(2))
 
127
        if versioned == (False, False):
 
128
            delta.unversioned.append((path[1], None, kind[1]))
 
129
            continue
 
130
        if not include_root and (None, None) == parent_id:
 
131
            continue
 
132
        fully_present = tuple((versioned[x] and kind[x] is not None) for
 
133
                              x in range(2))
125
134
        if fully_present[0] != fully_present[1]:
126
135
            if fully_present[1] is True:
127
 
                delta.added.append(change)
 
136
                delta.added.append((path[1], file_id, kind[1]))
128
137
            else:
129
 
                if change.kind[0] == 'symlink' and not new_tree.supports_symlinks():
130
 
                    trace.warning(
131
 
                        'Ignoring "%s" as symlinks '
132
 
                        'are not supported on this filesystem.' % (change.path[0],))
133
 
                else:
134
 
                    delta.removed.append(change)
 
138
                delta.removed.append((path[0], file_id, kind[0]))
135
139
        elif fully_present[0] is False:
136
 
            delta.missing.append(change)
137
 
        elif change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]:
138
 
            # If the name changes, or the parent_id changes, we have a rename or copy
 
140
            continue
 
141
        elif name[0] != name[1] or parent_id[0] != parent_id[1]:
 
142
            # If the name changes, or the parent_id changes, we have a rename
139
143
            # (if we move a parent, that doesn't count as a rename for the
140
144
            # file)
141
 
            if change.copied:
142
 
                delta.copied.append(change)
143
 
            else:
144
 
                delta.renamed.append(change)
145
 
        elif change.kind[0] != change.kind[1]:
146
 
            delta.kind_changed.append(change)
147
 
        elif change.changed_content or change.executable[0] != change.executable[1]:
148
 
            delta.modified.append(change)
149
 
        else:
150
 
            delta.unchanged.append(change)
151
 
 
152
 
    def change_key(change):
153
 
        if change.path[0] is None:
154
 
            path = change.path[1]
155
 
        else:
156
 
            path = change.path[0]
157
 
        return (path, change.file_id)
158
 
 
159
 
    delta.removed.sort(key=change_key)
160
 
    delta.added.sort(key=change_key)
161
 
    delta.renamed.sort(key=change_key)
162
 
    delta.copied.sort(key=change_key)
163
 
    delta.missing.sort(key=change_key)
 
145
            delta.renamed.append((path[0],
 
146
                                  path[1],
 
147
                                  file_id,
 
148
                                  kind[1],
 
149
                                  content_change,
 
150
                                  (executable[0] != executable[1])))
 
151
        elif kind[0] != kind[1]:
 
152
            delta.kind_changed.append((path[1], file_id, kind[0], kind[1]))
 
153
        elif content_change or executable[0] != executable[1]:
 
154
            delta.modified.append((path[1], file_id, kind[1],
 
155
                                   content_change,
 
156
                                   (executable[0] != executable[1])))
 
157
        else:
 
158
            delta.unchanged.append((path[1], file_id, kind[1]))
 
159
 
 
160
    delta.removed.sort()
 
161
    delta.added.sort()
 
162
    delta.renamed.sort()
164
163
    # TODO: jam 20060529 These lists shouldn't need to be sorted
165
164
    #       since we added them in alphabetical order.
166
 
    delta.modified.sort(key=change_key)
167
 
    delta.unchanged.sort(key=change_key)
168
 
    delta.unversioned.sort(key=change_key)
 
165
    delta.modified.sort()
 
166
    delta.unchanged.sort()
169
167
 
170
168
    return delta
171
169
 
174
172
    """Report changes between two trees"""
175
173
 
176
174
    def __init__(self, output=None, suppress_root_add=True,
177
 
                 output_file=None, unversioned_filter=None, view_info=None,
178
 
                 classify=True):
 
175
                 output_file=None, unversioned_filter=None, view_info=None):
179
176
        """Constructor
180
177
 
181
178
        :param output: a function with the signature of trace.note, i.e.
190
187
        :param view_info: A tuple of view_name,view_files if only
191
188
            items inside a view are to be reported on, or None for
192
189
            no view filtering.
193
 
        :param classify: Add special symbols to indicate file kind.
194
190
        """
195
191
        if output_file is not None:
196
192
            if output is not None:
197
193
                raise BzrError('Cannot specify both output and output_file')
198
 
 
199
194
            def output(fmt, *args):
200
195
                output_file.write((fmt % args) + '\n')
201
196
        self.output = output
202
197
        if self.output is None:
203
 
            from . import trace
 
198
            from bzrlib import trace
204
199
            self.output = trace.note
205
200
        self.suppress_root_add = suppress_root_add
206
201
        self.modified_map = {'kind changed': 'K',
207
202
                             'unchanged': ' ',
208
203
                             'created': 'N',
209
204
                             'modified': 'M',
210
 
                             'deleted': 'D',
211
 
                             'missing': '!',
212
 
                             }
213
 
        self.versioned_map = {'added': '+',  # versioned target
214
 
                              'unchanged': ' ',  # versioned in both
215
 
                              'removed': '-',  # versioned in source
216
 
                              'unversioned': '?',  # versioned in neither
 
205
                             'deleted': 'D'}
 
206
        self.versioned_map = {'added': '+', # versioned target
 
207
                              'unchanged': ' ', # versioned in both
 
208
                              'removed': '-', # versioned in source
 
209
                              'unversioned': '?', # versioned in neither
217
210
                              }
218
211
        self.unversioned_filter = unversioned_filter
219
 
        if classify:
220
 
            self.kind_marker = osutils.kind_marker
221
 
        else:
222
 
            self.kind_marker = lambda kind: ''
223
212
        if view_info is None:
224
213
            self.view_name = None
225
214
            self.view_files = []
229
218
            self.output("Operating on whole tree but only reporting on "
230
219
                        "'%s' view." % (self.view_name,))
231
220
 
232
 
    def report(self, paths, versioned, renamed, copied, modified, exe_change,
 
221
    def report(self, file_id, paths, versioned, renamed, modified, exe_change,
233
222
               kind):
234
223
        """Report one change to a file
235
224
 
 
225
        :param file_id: The file_id of the file
236
226
        :param path: The old and new paths as generated by Tree.iter_changes.
237
227
        :param versioned: may be 'added', 'removed', 'unchanged', or
238
228
            'unversioned.
239
229
        :param renamed: may be True or False
240
 
        :param copied: may be True or False
241
230
        :param modified: may be 'created', 'deleted', 'kind changed',
242
231
            'modified' or 'unchanged'.
243
232
        :param exe_change: True if the execute bit has changed
244
233
        :param kind: A pair of file kinds, as generated by Tree.iter_changes.
245
234
            None indicates no file present.
246
235
        """
247
 
        if trace.is_quiet():
 
236
        if is_quiet():
248
237
            return
249
238
        if paths[1] == '' and versioned == 'added' and self.suppress_root_add:
250
239
            return
251
240
        if self.view_files and not osutils.is_inside_any(self.view_files,
252
 
                                                         paths[1]):
 
241
            paths[1]):
253
242
            return
254
243
        if versioned == 'unversioned':
255
244
            # skip ignored unversioned files if needed.
263
252
        # ( the path is different OR
264
253
        #   the kind is different)
265
254
        if (versioned == 'unchanged' and
266
 
                (renamed or copied or modified == 'kind changed')):
267
 
            if renamed or copied:
268
 
                # on a rename or copy, we show old and new
 
255
            (renamed or modified == 'kind changed')):
 
256
            if renamed:
 
257
                # on a rename, we show old and new
269
258
                old_path, path = paths
270
259
            else:
271
 
                # if it's not renamed or copied, we're showing both for kind
272
 
                # changes so only show the new path
 
260
                # if it's not renamed, we're showing both for kind changes
 
261
                # so only show the new path
273
262
                old_path, path = paths[1], paths[1]
274
263
            # if the file is not missing in the source, we show its kind
275
264
            # when we show two paths.
276
265
            if kind[0] is not None:
277
 
                old_path += self.kind_marker(kind[0])
 
266
                old_path += osutils.kind_marker(kind[0])
278
267
            old_path += " => "
279
268
        elif versioned == 'removed':
280
269
            # not present in target
285
274
            path = paths[1]
286
275
        if renamed:
287
276
            rename = "R"
288
 
        elif copied:
289
 
            rename = "C"
290
277
        else:
291
278
            rename = self.versioned_map[versioned]
292
279
        # we show the old kind on the new path when the content is deleted.
293
280
        if modified == 'deleted':
294
 
            path += self.kind_marker(kind[0])
 
281
            path += osutils.kind_marker(kind[0])
295
282
        # otherwise we always show the current kind when there is one
296
283
        elif kind[1] is not None:
297
 
            path += self.kind_marker(kind[1])
 
284
            path += osutils.kind_marker(kind[1])
298
285
        if exe_change:
299
286
            exe = '*'
300
287
        else:
302
289
        self.output("%s%s%s %s%s", rename, self.modified_map[modified], exe,
303
290
                    old_path, path)
304
291
 
305
 
 
306
292
def report_changes(change_iterator, reporter):
307
293
    """Report the changes from a change iterator.
308
294
 
314
300
    :param reporter: The _ChangeReporter that will report the changes.
315
301
    """
316
302
    versioned_change_map = {
317
 
        (True, True): 'unchanged',
318
 
        (True, False): 'removed',
319
 
        (False, True): 'added',
 
303
        (True, True)  : 'unchanged',
 
304
        (True, False) : 'removed',
 
305
        (False, True) : 'added',
320
306
        (False, False): 'unversioned',
321
307
        }
322
 
 
323
 
    def path_key(change):
324
 
        if change.path[0] is not None:
325
 
            path = change.path[0]
326
 
        else:
327
 
            path = change.path[1]
328
 
        return osutils.splitpath(path)
329
 
    for change in sorted(change_iterator, key=path_key):
 
308
    for (file_id, path, content_change, versioned, parent_id, name, kind,
 
309
         executable) in change_iterator:
330
310
        exe_change = False
331
311
        # files are "renamed" if they are moved or if name changes, as long
332
312
        # as it had a value
333
 
        if None not in change.name and None not in change.parent_id and\
334
 
                (change.name[0] != change.name[1] or change.parent_id[0] != change.parent_id[1]):
335
 
            if change.copied:
336
 
                copied = True
337
 
                renamed = False
338
 
            else:
339
 
                renamed = True
340
 
                copied = False
 
313
        if None not in name and None not in parent_id and\
 
314
            (name[0] != name[1] or parent_id[0] != parent_id[1]):
 
315
            renamed = True
341
316
        else:
342
 
            copied = False
343
317
            renamed = False
344
 
        if change.kind[0] != change.kind[1]:
345
 
            if change.kind[0] is None:
 
318
        if kind[0] != kind[1]:
 
319
            if kind[0] is None:
346
320
                modified = "created"
347
 
            elif change.kind[1] is None:
 
321
            elif kind[1] is None:
348
322
                modified = "deleted"
349
323
            else:
350
324
                modified = "kind changed"
351
325
        else:
352
 
            if change.changed_content:
 
326
            if content_change:
353
327
                modified = "modified"
354
 
            elif change.kind[0] is None:
355
 
                modified = "missing"
356
328
            else:
357
329
                modified = "unchanged"
358
 
            if change.kind[1] == "file":
359
 
                exe_change = (change.executable[0] != change.executable[1])
360
 
        versioned_change = versioned_change_map[change.versioned]
361
 
        reporter.report(change.path, versioned_change, renamed, copied, modified,
362
 
                        exe_change, change.kind)
363
 
 
364
 
 
365
 
def report_delta(to_file, delta, short_status=False, show_ids=False,
366
 
                 show_unchanged=False, indent='', predicate=None, classify=True):
 
330
            if kind[1] == "file":
 
331
                exe_change = (executable[0] != executable[1])
 
332
        versioned_change = versioned_change_map[versioned]
 
333
        reporter.report(file_id, path, versioned_change, renamed, modified,
 
334
                        exe_change, kind)
 
335
 
 
336
def report_delta(to_file, delta, short_status=False, show_ids=False, 
 
337
         show_unchanged=False, indent='', filter=None):
367
338
    """Output this delta in status-like form to to_file.
368
339
 
369
340
    :param to_file: A file-like object where the output is displayed.
379
350
    :param indent: Added at the beginning of all output lines (for merged
380
351
        revisions).
381
352
 
382
 
    :param predicate: A callable receiving a path returning True if the path
383
 
        should be displayed.
384
 
 
385
 
    :param classify: Add special symbols to indicate file kind.
 
353
    :param filter: A callable receiving a path and a file id and
 
354
        returning True if the path should be displayed.
386
355
    """
387
356
 
388
357
    def decorate_path(path, kind, meta_modified=None):
389
 
        if not classify:
390
 
            return path
391
358
        if kind == 'directory':
392
359
            path += '/'
393
360
        elif kind == 'symlink':
397
364
        return path
398
365
 
399
366
    def show_more_renamed(item):
400
 
        dec_new_path = decorate_path(item.path[1], item.kind[1], item.meta_modified())
 
367
        (oldpath, file_id, kind,
 
368
         text_modified, meta_modified, newpath) = item
 
369
        dec_new_path = decorate_path(newpath, kind, meta_modified)
401
370
        to_file.write(' => %s' % dec_new_path)
402
 
        if item.changed_content or item.meta_modified():
403
 
            extra_modified.append(TreeChange(
404
 
                item.file_id, (item.path[1], item.path[1]),
405
 
                item.changed_content,
406
 
                item.versioned,
407
 
                (item.parent_id[1], item.parent_id[1]),
408
 
                (item.name[1], item.name[1]),
409
 
                (item.kind[1], item.kind[1]),
410
 
                item.executable))
 
371
        if text_modified or meta_modified:
 
372
            extra_modified.append((newpath, file_id, kind,
 
373
                                   text_modified, meta_modified))
411
374
 
412
375
    def show_more_kind_changed(item):
413
 
        to_file.write(' (%s => %s)' % (item.kind[0], item.kind[1]))
 
376
        (path, file_id, old_kind, new_kind) = item
 
377
        to_file.write(' (%s => %s)' % (old_kind, new_kind))
414
378
 
415
 
    def show_path(path, kind, meta_modified,
 
379
    def show_path(path, file_id, kind, meta_modified,
416
380
                  default_format, with_file_id_format):
417
381
        dec_path = decorate_path(path, kind, meta_modified)
418
382
        if show_ids:
432
396
            prefix = indent + prefix + '  '
433
397
 
434
398
            for item in files:
435
 
                if item.path[0] is None:
436
 
                    path = item.path[1]
437
 
                    kind = item.kind[1]
438
 
                else:
439
 
                    path = item.path[0]
440
 
                    kind = item.kind[0]
441
 
                if predicate is not None and not predicate(path):
 
399
                path, file_id, kind = item[:3]
 
400
                if (filter is not None and not filter(path, file_id)):
442
401
                    continue
443
402
                if not header_shown and not short_status:
444
403
                    to_file.write(indent + long_status_name + ':\n')
445
404
                    header_shown = True
 
405
                meta_modified = None
 
406
                if len(item) == 5:
 
407
                    meta_modified = item[4]
 
408
 
446
409
                to_file.write(prefix)
447
 
                show_path(path, kind, item.meta_modified(),
 
410
                show_path(path, file_id, kind, meta_modified,
448
411
                          default_format, with_file_id_format)
449
412
                if show_more is not None:
450
413
                    show_more(item)
451
 
                if show_ids and getattr(item, 'file_id', None):
452
 
                    to_file.write(' %s' % item.file_id.decode('utf-8'))
 
414
                if show_ids:
 
415
                    to_file.write(' %s' % file_id)
453
416
                to_file.write('\n')
454
417
 
455
418
    show_list(delta.removed, 'removed', 'D')
456
419
    show_list(delta.added, 'added', 'A')
457
 
    show_list(delta.missing, 'missing', '!')
458
420
    extra_modified = []
459
 
    show_list(delta.renamed, 'renamed', 'R', with_file_id_format='%s',
460
 
              show_more=show_more_renamed)
461
 
    show_list(delta.copied, 'copied', 'C', with_file_id_format='%s',
 
421
    # Reorder delta.renamed tuples so that all lists share the same
 
422
    # order for their 3 first fields and that they also begin like
 
423
    # the delta.modified tuples
 
424
    renamed = [(p, i, k, tm, mm, np)
 
425
               for  p, np, i, k, tm, mm  in delta.renamed]
 
426
    show_list(renamed, 'renamed', 'R', with_file_id_format='%s',
462
427
              show_more=show_more_renamed)
463
428
    show_list(delta.kind_changed, 'kind changed', 'K',
464
429
              with_file_id_format='%s',
468
433
        show_list(delta.unchanged, 'unchanged', 'S')
469
434
 
470
435
    show_list(delta.unversioned, 'unknown', ' ')
 
436