/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: 2018-05-19 13:16:11 UTC
  • mto: (6968.4.3 git-archive)
  • mto: This revision was merged to the branch mainline in revision 6972.
  • Revision ID: jelmer@jelmer.uk-20180519131611-l9h9ud41j7qg1m03
Move tar/zip to breezy.archive.

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