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

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-07-20 08:56:45 UTC
  • mfrom: (4526.9.23 apply-inventory-delta)
  • Revision ID: pqm@pqm.ubuntu.com-20090720085645-54mtgybxua0yx6hw
(robertc) Add checks for inventory deltas which try to ensure that
        deltas that are not an exact fit are not applied. (Robert
        Collins, bug 397705, bug 367633)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2008 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
 
 
18
from cStringIO import StringIO
 
19
import shutil
 
20
import sys
 
21
import tempfile
 
22
 
 
23
from bzrlib import (
 
24
    builtins,
 
25
    delta,
 
26
    diff,
 
27
    errors,
 
28
    osutils,
 
29
    patches,
 
30
    shelf,
 
31
    textfile,
 
32
    trace,
 
33
    ui,
 
34
    workingtree,
 
35
)
 
36
 
 
37
 
 
38
class ShelfReporter(object):
 
39
 
 
40
    vocab = {'add file': 'Shelve adding file "%(path)s"?',
 
41
             'binary': 'Shelve binary changes?',
 
42
             'change kind': 'Shelve changing "%s" from %(other)s'
 
43
             ' to %(this)s?',
 
44
             'delete file': 'Shelve removing file "%(path)s"?',
 
45
             'final': 'Shelve %d change(s)?',
 
46
             'hunk': 'Shelve?',
 
47
             'modify target': 'Shelve changing target of'
 
48
             ' "%(path)s" from "%(other)s" to "%(this)s"?',
 
49
             'rename': 'Shelve renaming "%(other)s" =>'
 
50
                        ' "%(this)s"?'
 
51
             }
 
52
 
 
53
    invert_diff = False
 
54
 
 
55
    def __init__(self):
 
56
        self.delta_reporter = delta._ChangeReporter()
 
57
 
 
58
    def no_changes(self):
 
59
        """Report that no changes were selected to apply."""
 
60
        trace.warning('No changes to shelve.')
 
61
 
 
62
    def shelved_id(self, shelf_id):
 
63
        """Report the id changes were shelved to."""
 
64
        trace.note('Changes shelved with id "%d".' % shelf_id)
 
65
 
 
66
    def changes_destroyed(self):
 
67
        """Report that changes were made without shelving."""
 
68
        trace.note('Selected changes destroyed.')
 
69
 
 
70
    def selected_changes(self, transform):
 
71
        """Report the changes that were selected."""
 
72
        trace.note("Selected changes:")
 
73
        changes = transform.iter_changes()
 
74
        delta.report_changes(changes, self.delta_reporter)
 
75
 
 
76
    def prompt_change(self, change):
 
77
        """Determine the prompt for a change to apply."""
 
78
        if change[0] == 'rename':
 
79
            vals = {'this': change[3], 'other': change[2]}
 
80
        elif change[0] == 'change kind':
 
81
            vals = {'path': change[4], 'other': change[2], 'this': change[3]}
 
82
        elif change[0] == 'modify target':
 
83
            vals = {'path': change[2], 'other': change[3], 'this': change[4]}
 
84
        else:
 
85
            vals = {'path': change[3]}
 
86
        prompt = self.vocab[change[0]] % vals
 
87
        return prompt
 
88
 
 
89
 
 
90
class ApplyReporter(ShelfReporter):
 
91
 
 
92
    vocab = {'add file': 'Delete file "%(path)s"?',
 
93
             'binary': 'Apply binary changes?',
 
94
             'change kind': 'Change "%(path)s" from %(this)s'
 
95
             ' to %(other)s?',
 
96
             'delete file': 'Add file "%(path)s"?',
 
97
             'final': 'Apply %d change(s)?',
 
98
             'hunk': 'Apply change?',
 
99
             'modify target': 'Change target of'
 
100
             ' "%(path)s" from "%(this)s" to "%(other)s"?',
 
101
             'rename': 'Rename "%(this)s" => "%(other)s"?',
 
102
             }
 
103
 
 
104
    invert_diff = True
 
105
 
 
106
    def changes_destroyed(self):
 
107
        pass
 
108
 
 
109
 
 
110
class Shelver(object):
 
111
    """Interactively shelve the changes in a working tree."""
 
112
 
 
113
    def __init__(self, work_tree, target_tree, diff_writer=None, auto=False,
 
114
                 auto_apply=False, file_list=None, message=None,
 
115
                 destroy=False, manager=None, reporter=None):
 
116
        """Constructor.
 
117
 
 
118
        :param work_tree: The working tree to shelve changes from.
 
119
        :param target_tree: The "unchanged" / old tree to compare the
 
120
            work_tree to.
 
121
        :param auto: If True, shelve each possible change.
 
122
        :param auto_apply: If True, shelve changes with no final prompt.
 
123
        :param file_list: If supplied, only files in this list may be shelved.
 
124
        :param message: The message to associate with the shelved changes.
 
125
        :param destroy: Change the working tree without storing the shelved
 
126
            changes.
 
127
        :param manager: The shelf manager to use.
 
128
        :param reporter: Object for reporting changes to user.
 
129
        """
 
130
        self.work_tree = work_tree
 
131
        self.target_tree = target_tree
 
132
        self.diff_writer = diff_writer
 
133
        if self.diff_writer is None:
 
134
            self.diff_writer = sys.stdout
 
135
        if manager is None:
 
136
            manager = work_tree.get_shelf_manager()
 
137
        self.manager = manager
 
138
        self.auto = auto
 
139
        self.auto_apply = auto_apply
 
140
        self.file_list = file_list
 
141
        self.message = message
 
142
        self.destroy = destroy
 
143
        if reporter is None:
 
144
            reporter = ShelfReporter()
 
145
        self.reporter = reporter
 
146
 
 
147
    @classmethod
 
148
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
 
149
                  message=None, directory='.', destroy=False):
 
150
        """Create a shelver from commandline arguments.
 
151
 
 
152
        :param revision: RevisionSpec of the revision to compare to.
 
153
        :param all: If True, shelve all changes without prompting.
 
154
        :param file_list: If supplied, only files in this list may be  shelved.
 
155
        :param message: The message to associate with the shelved changes.
 
156
        :param directory: The directory containing the working tree.
 
157
        :param destroy: Change the working tree without storing the shelved
 
158
            changes.
 
159
        """
 
160
        tree, path = workingtree.WorkingTree.open_containing(directory)
 
161
        target_tree = builtins._get_one_revision_tree('shelf2', revision,
 
162
            tree.branch, tree)
 
163
        files = builtins.safe_relpath_files(tree, file_list)
 
164
        return klass(tree, target_tree, diff_writer, all, all, files, message,
 
165
                     destroy)
 
166
 
 
167
    def run(self):
 
168
        """Interactively shelve the changes."""
 
169
        creator = shelf.ShelfCreator(self.work_tree, self.target_tree,
 
170
                                     self.file_list)
 
171
        self.tempdir = tempfile.mkdtemp()
 
172
        changes_shelved = 0
 
173
        try:
 
174
            for change in creator.iter_shelvable():
 
175
                if change[0] == 'modify text':
 
176
                    try:
 
177
                        changes_shelved += self.handle_modify_text(creator,
 
178
                                                                   change[1])
 
179
                    except errors.BinaryFile:
 
180
                        if self.prompt_bool(self.reporter.vocab['binary']):
 
181
                            changes_shelved += 1
 
182
                            creator.shelve_content_change(change[1])
 
183
                else:
 
184
                    if self.prompt_bool(self.reporter.prompt_change(change)):
 
185
                        creator.shelve_change(change)
 
186
                        changes_shelved += 1
 
187
            if changes_shelved > 0:
 
188
                self.reporter.selected_changes(creator.work_transform)
 
189
                if (self.auto_apply or self.prompt_bool(
 
190
                    self.reporter.vocab['final'] % changes_shelved)):
 
191
                    if self.destroy:
 
192
                        creator.transform()
 
193
                        self.reporter.changes_destroyed()
 
194
                    else:
 
195
                        shelf_id = self.manager.shelve_changes(creator,
 
196
                                                               self.message)
 
197
                        self.reporter.shelved_id(shelf_id)
 
198
            else:
 
199
                self.reporter.no_changes()
 
200
        finally:
 
201
            shutil.rmtree(self.tempdir)
 
202
            creator.finalize()
 
203
 
 
204
    def get_parsed_patch(self, file_id, invert=False):
 
205
        """Return a parsed version of a file's patch.
 
206
 
 
207
        :param file_id: The id of the file to generate a patch for.
 
208
        :param invert: If True, provide an inverted patch (insertions displayed
 
209
            as removals, removals displayed as insertions).
 
210
        :return: A patches.Patch.
 
211
        """
 
212
        diff_file = StringIO()
 
213
        if invert:
 
214
            old_tree = self.work_tree
 
215
            new_tree = self.target_tree
 
216
        else:
 
217
            old_tree = self.target_tree
 
218
            new_tree = self.work_tree
 
219
        old_path = old_tree.id2path(file_id)
 
220
        new_path = new_tree.id2path(file_id)
 
221
        text_differ = diff.DiffText(old_tree, new_tree, diff_file)
 
222
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
 
223
        diff_file.seek(0)
 
224
        return patches.parse_patch(diff_file)
 
225
 
 
226
    def prompt(self, message):
 
227
        """Prompt the user for a character.
 
228
 
 
229
        :param message: The message to prompt a user with.
 
230
        :return: A character.
 
231
        """
 
232
        sys.stdout.write(message)
 
233
        char = osutils.getchar()
 
234
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
 
235
        sys.stdout.flush()
 
236
        return char
 
237
 
 
238
    def prompt_bool(self, question, long=False):
 
239
        """Prompt the user with a yes/no question.
 
240
 
 
241
        This may be overridden by self.auto.  It may also *set* self.auto.  It
 
242
        may also raise UserAbort.
 
243
        :param question: The question to ask the user.
 
244
        :return: True or False
 
245
        """
 
246
        if self.auto:
 
247
            return True
 
248
        if long:
 
249
            prompt = ' [(y)es, (N)o, (f)inish, or (q)uit]'
 
250
        else:
 
251
            prompt = ' [yNfq?]'
 
252
        char = self.prompt(question + prompt)
 
253
        if char == 'y':
 
254
            return True
 
255
        elif char == 'f':
 
256
            self.auto = True
 
257
            return True
 
258
        elif char == '?':
 
259
            return self.prompt_bool(question, long=True)
 
260
        if char == 'q':
 
261
            raise errors.UserAbort()
 
262
        else:
 
263
            return False
 
264
 
 
265
    def handle_modify_text(self, creator, file_id):
 
266
        """Provide diff hunk selection for modified text.
 
267
 
 
268
        If self.reporter.invert_diff is True, the diff is inverted so that
 
269
        insertions are displayed as removals and vice versa.
 
270
 
 
271
        :param creator: a ShelfCreator
 
272
        :param file_id: The id of the file to shelve.
 
273
        :return: number of shelved hunks.
 
274
        """
 
275
        if self.reporter.invert_diff:
 
276
            target_lines = self.work_tree.get_file_lines(file_id)
 
277
        else:
 
278
            target_lines = self.target_tree.get_file_lines(file_id)
 
279
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
 
280
        textfile.check_text_lines(target_lines)
 
281
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
 
282
        final_hunks = []
 
283
        if not self.auto:
 
284
            offset = 0
 
285
            self.diff_writer.write(parsed.get_header())
 
286
            for hunk in parsed.hunks:
 
287
                self.diff_writer.write(str(hunk))
 
288
                selected = self.prompt_bool(self.reporter.vocab['hunk'])
 
289
                if not self.reporter.invert_diff:
 
290
                    selected = (not selected)
 
291
                if selected:
 
292
                    hunk.mod_pos += offset
 
293
                    final_hunks.append(hunk)
 
294
                else:
 
295
                    offset -= (hunk.mod_range - hunk.orig_range)
 
296
        sys.stdout.flush()
 
297
        if not self.reporter.invert_diff and (
 
298
            len(parsed.hunks) == len(final_hunks)):
 
299
            return 0
 
300
        if self.reporter.invert_diff and len(final_hunks) == 0:
 
301
            return 0
 
302
        patched = patches.iter_patched_from_hunks(target_lines, final_hunks)
 
303
        creator.shelve_lines(file_id, list(patched))
 
304
        if self.reporter.invert_diff:
 
305
            return len(final_hunks)
 
306
        return len(parsed.hunks) - len(final_hunks)
 
307
 
 
308
 
 
309
class Unshelver(object):
 
310
    """Unshelve changes into a working tree."""
 
311
 
 
312
    @classmethod
 
313
    def from_args(klass, shelf_id=None, action='apply', directory='.'):
 
314
        """Create an unshelver from commandline arguments.
 
315
 
 
316
        :param shelf_id: Integer id of the shelf, as a string.
 
317
        :param action: action to perform.  May be 'apply', 'dry-run',
 
318
            'delete'.
 
319
        :param directory: The directory to unshelve changes into.
 
320
        """
 
321
        tree, path = workingtree.WorkingTree.open_containing(directory)
 
322
        manager = tree.get_shelf_manager()
 
323
        if shelf_id is not None:
 
324
            try:
 
325
                shelf_id = int(shelf_id)
 
326
            except ValueError:
 
327
                raise errors.InvalidShelfId(shelf_id)
 
328
        else:
 
329
            shelf_id = manager.last_shelf()
 
330
            if shelf_id is None:
 
331
                raise errors.BzrCommandError('No changes are shelved.')
 
332
            trace.note('Unshelving changes with id "%d".' % shelf_id)
 
333
        apply_changes = True
 
334
        delete_shelf = True
 
335
        read_shelf = True
 
336
        if action == 'dry-run':
 
337
            apply_changes = False
 
338
            delete_shelf = False
 
339
        if action == 'delete-only':
 
340
            apply_changes = False
 
341
            read_shelf = False
 
342
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
 
343
                     read_shelf)
 
344
 
 
345
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
 
346
                 delete_shelf=True, read_shelf=True):
 
347
        """Constructor.
 
348
 
 
349
        :param tree: The working tree to unshelve into.
 
350
        :param manager: The ShelveManager containing the shelved changes.
 
351
        :param shelf_id:
 
352
        :param apply_changes: If True, apply the shelved changes to the
 
353
            working tree.
 
354
        :param delete_shelf: If True, delete the changes from the shelf.
 
355
        :param read_shelf: If True, read the changes from the shelf.
 
356
        """
 
357
        self.tree = tree
 
358
        manager = tree.get_shelf_manager()
 
359
        self.manager = manager
 
360
        self.shelf_id = shelf_id
 
361
        self.apply_changes = apply_changes
 
362
        self.delete_shelf = delete_shelf
 
363
        self.read_shelf = read_shelf
 
364
 
 
365
    def run(self):
 
366
        """Perform the unshelving operation."""
 
367
        self.tree.lock_write()
 
368
        cleanups = [self.tree.unlock]
 
369
        try:
 
370
            if self.read_shelf:
 
371
                unshelver = self.manager.get_unshelver(self.shelf_id)
 
372
                cleanups.append(unshelver.finalize)
 
373
                if unshelver.message is not None:
 
374
                    trace.note('Message: %s' % unshelver.message)
 
375
                change_reporter = delta._ChangeReporter()
 
376
                task = ui.ui_factory.nested_progress_bar()
 
377
                try:
 
378
                    merger = unshelver.make_merger(task)
 
379
                    merger.change_reporter = change_reporter
 
380
                    if self.apply_changes:
 
381
                        merger.do_merge()
 
382
                    else:
 
383
                        self.show_changes(merger)
 
384
                finally:
 
385
                    task.finished()
 
386
            if self.delete_shelf:
 
387
                self.manager.delete_shelf(self.shelf_id)
 
388
        finally:
 
389
            for cleanup in reversed(cleanups):
 
390
                cleanup()
 
391
 
 
392
    def show_changes(self, merger):
 
393
        """Show the changes that this operation specifies."""
 
394
        tree_merger = merger.make_merger()
 
395
        # This implicitly shows the changes via the reporter, so we're done...
 
396
        tt = tree_merger.make_preview_transform()
 
397
        tt.finalize()