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

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2020-08-23 01:15:41 UTC
  • mfrom: (7520.1.4 merge-3.1)
  • Revision ID: breezy.the.bot@gmail.com-20200823011541-nv0oh7nzaganx2qy
Merge lp:brz/3.1.

Merged from https://code.launchpad.net/~jelmer/brz/merge-3.1/+merge/389690

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