/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-19 23:18:42 UTC
  • mto: (7490.3.4 work)
  • mto: This revision was merged to the branch mainline in revision 7495.
  • Revision ID: jelmer@jelmer.uk-20200219231842-agwjh2db66cpajqg
Consistent return values.

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 breezy import (
 
20
    osutils,
 
21
    trace,
 
22
    )
 
23
from .sixish import (
 
24
    StringIO,
 
25
    )
 
26
from .tree import TreeChange
 
27
 
 
28
 
 
29
class TreeDelta(object):
 
30
    """Describes changes from one tree to another.
 
31
 
 
32
    Contains seven lists with TreeChange objects.
 
33
 
 
34
    added
 
35
    removed
 
36
    renamed
 
37
    copied
 
38
    kind_changed
 
39
    modified
 
40
    unchanged
 
41
    unversioned
 
42
 
 
43
    Each id is listed only once.
 
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
 
47
    applies either to the content of the file or the target of the
 
48
    symbolic link, depending of the kind of file.
 
49
 
 
50
    Files are only considered renamed if their name has changed or
 
51
    their parent directory has changed.  Renaming a directory
 
52
    does not count as renaming all its contents.
 
53
 
 
54
    The lists are normally sorted when the delta is created.
 
55
    """
 
56
 
 
57
    def __init__(self):
 
58
        self.added = []
 
59
        self.removed = []
 
60
        self.renamed = []
 
61
        self.copied = []
 
62
        self.kind_changed = []
 
63
        self.modified = []
 
64
        self.unchanged = []
 
65
        self.unversioned = []
 
66
        self.missing = []
 
67
 
 
68
    def __eq__(self, other):
 
69
        if not isinstance(other, TreeDelta):
 
70
            return False
 
71
        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
 
79
 
 
80
    def __ne__(self, other):
 
81
        return not (self == other)
 
82
 
 
83
    def __repr__(self):
 
84
        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)
 
90
 
 
91
    def has_changed(self):
 
92
        return bool(self.modified
 
93
                    or self.added
 
94
                    or self.removed
 
95
                    or self.renamed
 
96
                    or self.copied
 
97
                    or self.kind_changed)
 
98
 
 
99
    def get_changes_as_text(self, show_ids=False, show_unchanged=False,
 
100
                            short_status=False):
 
101
        output = StringIO()
 
102
        report_delta(output, self, short_status, show_ids, show_unchanged)
 
103
        return output.getvalue()
 
104
 
 
105
 
 
106
def _compare_trees(old_tree, new_tree, want_unchanged, specific_files,
 
107
                   include_root, extra_trees=None,
 
108
                   require_versioned=False, want_unversioned=False):
 
109
    """Worker function that implements Tree.changes_from."""
 
110
    delta = TreeDelta()
 
111
    # mutter('start compare_trees')
 
112
 
 
113
    for change in new_tree.iter_changes(
 
114
            old_tree, want_unchanged, specific_files, extra_trees=extra_trees,
 
115
            require_versioned=require_versioned,
 
116
            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))
 
125
        if fully_present[0] != fully_present[1]:
 
126
            if fully_present[1] is True:
 
127
                delta.added.append(change)
 
128
            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)
 
135
        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
 
139
            # (if we move a parent, that doesn't count as a rename for the
 
140
            # 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)
 
164
    # TODO: jam 20060529 These lists shouldn't need to be sorted
 
165
    #       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)
 
169
 
 
170
    return delta
 
171
 
 
172
 
 
173
class _ChangeReporter(object):
 
174
    """Report changes between two trees"""
 
175
 
 
176
    def __init__(self, output=None, suppress_root_add=True,
 
177
                 output_file=None, unversioned_filter=None, view_info=None,
 
178
                 classify=True):
 
179
        """Constructor
 
180
 
 
181
        :param output: a function with the signature of trace.note, i.e.
 
182
            accepts a format and parameters.
 
183
        :param supress_root_add: If true, adding the root will be ignored
 
184
            (i.e. when a tree has just been initted)
 
185
        :param output_file: If supplied, a file-like object to write to.
 
186
            Only one of output and output_file may be supplied.
 
187
        :param unversioned_filter: A filter function to be called on
 
188
            unversioned files. This should return True to ignore a path.
 
189
            By default, no filtering takes place.
 
190
        :param view_info: A tuple of view_name,view_files if only
 
191
            items inside a view are to be reported on, or None for
 
192
            no view filtering.
 
193
        :param classify: Add special symbols to indicate file kind.
 
194
        """
 
195
        if output_file is not None:
 
196
            if output is not None:
 
197
                raise BzrError('Cannot specify both output and output_file')
 
198
 
 
199
            def output(fmt, *args):
 
200
                output_file.write((fmt % args) + '\n')
 
201
        self.output = output
 
202
        if self.output is None:
 
203
            from . import trace
 
204
            self.output = trace.note
 
205
        self.suppress_root_add = suppress_root_add
 
206
        self.modified_map = {'kind changed': 'K',
 
207
                             'unchanged': ' ',
 
208
                             'created': 'N',
 
209
                             '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
 
217
                              }
 
218
        self.unversioned_filter = unversioned_filter
 
219
        if classify:
 
220
            self.kind_marker = osutils.kind_marker
 
221
        else:
 
222
            self.kind_marker = lambda kind: ''
 
223
        if view_info is None:
 
224
            self.view_name = None
 
225
            self.view_files = []
 
226
        else:
 
227
            self.view_name = view_info[0]
 
228
            self.view_files = view_info[1]
 
229
            self.output("Operating on whole tree but only reporting on "
 
230
                        "'%s' view." % (self.view_name,))
 
231
 
 
232
    def report(self, paths, versioned, renamed, copied, modified, exe_change,
 
233
               kind):
 
234
        """Report one change to a file
 
235
 
 
236
        :param path: The old and new paths as generated by Tree.iter_changes.
 
237
        :param versioned: may be 'added', 'removed', 'unchanged', or
 
238
            'unversioned.
 
239
        :param renamed: may be True or False
 
240
        :param copied: may be True or False
 
241
        :param modified: may be 'created', 'deleted', 'kind changed',
 
242
            'modified' or 'unchanged'.
 
243
        :param exe_change: True if the execute bit has changed
 
244
        :param kind: A pair of file kinds, as generated by Tree.iter_changes.
 
245
            None indicates no file present.
 
246
        """
 
247
        if trace.is_quiet():
 
248
            return
 
249
        if paths[1] == '' and versioned == 'added' and self.suppress_root_add:
 
250
            return
 
251
        if self.view_files and not osutils.is_inside_any(self.view_files,
 
252
                                                         paths[1]):
 
253
            return
 
254
        if versioned == 'unversioned':
 
255
            # skip ignored unversioned files if needed.
 
256
            if self.unversioned_filter is not None:
 
257
                if self.unversioned_filter(paths[1]):
 
258
                    return
 
259
            # dont show a content change in the output.
 
260
            modified = 'unchanged'
 
261
        # we show both paths in the following situations:
 
262
        # the file versioning is unchanged AND
 
263
        # ( the path is different OR
 
264
        #   the kind is different)
 
265
        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
 
269
                old_path, path = paths
 
270
            else:
 
271
                # if it's not renamed or copied, we're showing both for kind
 
272
                # changes so only show the new path
 
273
                old_path, path = paths[1], paths[1]
 
274
            # if the file is not missing in the source, we show its kind
 
275
            # when we show two paths.
 
276
            if kind[0] is not None:
 
277
                old_path += self.kind_marker(kind[0])
 
278
            old_path += " => "
 
279
        elif versioned == 'removed':
 
280
            # not present in target
 
281
            old_path = ""
 
282
            path = paths[0]
 
283
        else:
 
284
            old_path = ""
 
285
            path = paths[1]
 
286
        if renamed:
 
287
            rename = "R"
 
288
        elif copied:
 
289
            rename = "C"
 
290
        else:
 
291
            rename = self.versioned_map[versioned]
 
292
        # we show the old kind on the new path when the content is deleted.
 
293
        if modified == 'deleted':
 
294
            path += self.kind_marker(kind[0])
 
295
        # otherwise we always show the current kind when there is one
 
296
        elif kind[1] is not None:
 
297
            path += self.kind_marker(kind[1])
 
298
        if exe_change:
 
299
            exe = '*'
 
300
        else:
 
301
            exe = ' '
 
302
        self.output("%s%s%s %s%s", rename, self.modified_map[modified], exe,
 
303
                    old_path, path)
 
304
 
 
305
 
 
306
def report_changes(change_iterator, reporter):
 
307
    """Report the changes from a change iterator.
 
308
 
 
309
    This is essentially a translation from low-level to medium-level changes.
 
310
    Further processing may be required to produce a human-readable output.
 
311
    Unfortunately, some tree-changing operations are very complex
 
312
    :change_iterator: an iterator or sequence of changes in the format
 
313
        generated by Tree.iter_changes
 
314
    :param reporter: The _ChangeReporter that will report the changes.
 
315
    """
 
316
    versioned_change_map = {
 
317
        (True, True): 'unchanged',
 
318
        (True, False): 'removed',
 
319
        (False, True): 'added',
 
320
        (False, False): 'unversioned',
 
321
        }
 
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):
 
330
        exe_change = False
 
331
        # files are "renamed" if they are moved or if name changes, as long
 
332
        # 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
 
341
        else:
 
342
            copied = False
 
343
            renamed = False
 
344
        if change.kind[0] != change.kind[1]:
 
345
            if change.kind[0] is None:
 
346
                modified = "created"
 
347
            elif change.kind[1] is None:
 
348
                modified = "deleted"
 
349
            else:
 
350
                modified = "kind changed"
 
351
        else:
 
352
            if change.changed_content:
 
353
                modified = "modified"
 
354
            elif change.kind[0] is None:
 
355
                modified = "missing"
 
356
            else:
 
357
                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):
 
367
    """Output this delta in status-like form to to_file.
 
368
 
 
369
    :param to_file: A file-like object where the output is displayed.
 
370
 
 
371
    :param delta: A TreeDelta containing the changes to be displayed
 
372
 
 
373
    :param short_status: Single-line status if True.
 
374
 
 
375
    :param show_ids: Output the file ids if True.
 
376
 
 
377
    :param show_unchanged: Output the unchanged files if True.
 
378
 
 
379
    :param indent: Added at the beginning of all output lines (for merged
 
380
        revisions).
 
381
 
 
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.
 
386
    """
 
387
 
 
388
    def decorate_path(path, kind, meta_modified=None):
 
389
        if not classify:
 
390
            return path
 
391
        if kind == 'directory':
 
392
            path += '/'
 
393
        elif kind == 'symlink':
 
394
            path += '@'
 
395
        if meta_modified:
 
396
            path += '*'
 
397
        return path
 
398
 
 
399
    def show_more_renamed(item):
 
400
        dec_new_path = decorate_path(item.path[1], item.kind[1], item.meta_modified())
 
401
        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))
 
411
 
 
412
    def show_more_kind_changed(item):
 
413
        to_file.write(' (%s => %s)' % (item.kind[0], item.kind[1]))
 
414
 
 
415
    def show_path(path, kind, meta_modified,
 
416
                  default_format, with_file_id_format):
 
417
        dec_path = decorate_path(path, kind, meta_modified)
 
418
        if show_ids:
 
419
            to_file.write(with_file_id_format % dec_path)
 
420
        else:
 
421
            to_file.write(default_format % dec_path)
 
422
 
 
423
    def show_list(files, long_status_name, short_status_letter,
 
424
                  default_format='%s', with_file_id_format='%-30s',
 
425
                  show_more=None):
 
426
        if files:
 
427
            header_shown = False
 
428
            if short_status:
 
429
                prefix = short_status_letter
 
430
            else:
 
431
                prefix = ''
 
432
            prefix = indent + prefix + '  '
 
433
 
 
434
            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):
 
442
                    continue
 
443
                if not header_shown and not short_status:
 
444
                    to_file.write(indent + long_status_name + ':\n')
 
445
                    header_shown = True
 
446
                to_file.write(prefix)
 
447
                show_path(path, kind, item.meta_modified(),
 
448
                          default_format, with_file_id_format)
 
449
                if show_more is not None:
 
450
                    show_more(item)
 
451
                if show_ids and getattr(item, 'file_id', None):
 
452
                    to_file.write(' %s' % item.file_id.decode('utf-8'))
 
453
                to_file.write('\n')
 
454
 
 
455
    show_list(delta.removed, 'removed', 'D')
 
456
    show_list(delta.added, 'added', 'A')
 
457
    show_list(delta.missing, 'missing', '!')
 
458
    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',
 
462
              show_more=show_more_renamed)
 
463
    show_list(delta.kind_changed, 'kind changed', 'K',
 
464
              with_file_id_format='%s',
 
465
              show_more=show_more_kind_changed)
 
466
    show_list(delta.modified + extra_modified, 'modified', 'M')
 
467
    if show_unchanged:
 
468
        show_list(delta.unchanged, 'unchanged', 'S')
 
469
 
 
470
    show_list(delta.unversioned, 'unknown', ' ')