/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: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

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