~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/shelf_ui.py

(vila) Fix test failures blocking package builds. (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008 Canonical Ltd
 
1
# Copyright (C) 2008, 2009, 2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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 __future__ import absolute_import
17
18
 
18
19
from cStringIO import StringIO
19
20
import shutil
27
28
    errors,
28
29
    osutils,
29
30
    patches,
 
31
    patiencediff,
30
32
    shelf,
31
33
    textfile,
32
34
    trace,
33
35
    ui,
34
36
    workingtree,
35
37
)
 
38
from bzrlib.i18n import gettext
 
39
 
 
40
class UseEditor(Exception):
 
41
    """Use an editor instead of selecting hunks."""
36
42
 
37
43
 
38
44
class ShelfReporter(object):
39
45
 
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"?'
 
46
    vocab = {'add file': gettext('Shelve adding file "%(path)s"?'),
 
47
             'binary': gettext('Shelve binary changes?'),
 
48
             'change kind': gettext('Shelve changing "%s" from %(other)s'
 
49
             ' to %(this)s?'),
 
50
             'delete file': gettext('Shelve removing file "%(path)s"?'),
 
51
             'final': gettext('Shelve %d change(s)?'),
 
52
             'hunk': gettext('Shelve?'),
 
53
             'modify target': gettext('Shelve changing target of'
 
54
             ' "%(path)s" from "%(other)s" to "%(this)s"?'),
 
55
             'rename': gettext('Shelve renaming "%(other)s" =>'
 
56
                        ' "%(this)s"?')
51
57
             }
52
58
 
53
59
    invert_diff = False
61
67
 
62
68
    def shelved_id(self, shelf_id):
63
69
        """Report the id changes were shelved to."""
64
 
        trace.note('Changes shelved with id "%d".' % shelf_id)
 
70
        trace.note(gettext('Changes shelved with id "%d".') % shelf_id)
65
71
 
66
72
    def changes_destroyed(self):
67
73
        """Report that changes were made without shelving."""
68
 
        trace.note('Selected changes destroyed.')
 
74
        trace.note(gettext('Selected changes destroyed.'))
69
75
 
70
76
    def selected_changes(self, transform):
71
77
        """Report the changes that were selected."""
72
 
        trace.note("Selected changes:")
 
78
        trace.note(gettext("Selected changes:"))
73
79
        changes = transform.iter_changes()
74
80
        delta.report_changes(changes, self.delta_reporter)
75
81
 
89
95
 
90
96
class ApplyReporter(ShelfReporter):
91
97
 
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"?',
 
98
    vocab = {'add file': gettext('Delete file "%(path)s"?'),
 
99
             'binary': gettext('Apply binary changes?'),
 
100
             'change kind': gettext('Change "%(path)s" from %(this)s'
 
101
             ' to %(other)s?'),
 
102
             'delete file': gettext('Add file "%(path)s"?'),
 
103
             'final': gettext('Apply %d change(s)?'),
 
104
             'hunk': gettext('Apply change?'),
 
105
             'modify target': gettext('Change target of'
 
106
             ' "%(path)s" from "%(this)s" to "%(other)s"?'),
 
107
             'rename': gettext('Rename "%(this)s" => "%(other)s"?'),
102
108
             }
103
109
 
104
110
    invert_diff = True
143
149
        if reporter is None:
144
150
            reporter = ShelfReporter()
145
151
        self.reporter = reporter
 
152
        config = self.work_tree.branch.get_config()
 
153
        self.change_editor = config.get_change_editor(target_tree, work_tree)
 
154
        self.work_tree.lock_tree_write()
146
155
 
147
156
    @classmethod
148
157
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
149
 
                  message=None, directory='.', destroy=False):
 
158
                  message=None, directory=None, destroy=False):
150
159
        """Create a shelver from commandline arguments.
151
160
 
 
161
        The returned shelver wil have a work_tree that is locked and should
 
162
        be unlocked.
 
163
 
152
164
        :param revision: RevisionSpec of the revision to compare to.
153
165
        :param all: If True, shelve all changes without prompting.
154
166
        :param file_list: If supplied, only files in this list may be  shelved.
157
169
        :param destroy: Change the working tree without storing the shelved
158
170
            changes.
159
171
        """
 
172
        if directory is None:
 
173
            directory = u'.'
 
174
        elif file_list:
 
175
            file_list = [osutils.pathjoin(directory, f) for f in file_list]
160
176
        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)
 
177
        # Ensure that tree is locked for the lifetime of target_tree, as
 
178
        # target tree may be reading from the same dirstate.
 
179
        tree.lock_tree_write()
 
180
        try:
 
181
            target_tree = builtins._get_one_revision_tree('shelf2', revision,
 
182
                tree.branch, tree)
 
183
            files = tree.safe_relpath_files(file_list)
 
184
            return klass(tree, target_tree, diff_writer, all, all, files,
 
185
                         message, destroy)
 
186
        finally:
 
187
            tree.unlock()
166
188
 
167
189
    def run(self):
168
190
        """Interactively shelve the changes."""
201
223
            shutil.rmtree(self.tempdir)
202
224
            creator.finalize()
203
225
 
 
226
    def finalize(self):
 
227
        if self.change_editor is not None:
 
228
            self.change_editor.finish()
 
229
        self.work_tree.unlock()
 
230
 
 
231
 
204
232
    def get_parsed_patch(self, file_id, invert=False):
205
233
        """Return a parsed version of a file's patch.
206
234
 
218
246
            new_tree = self.work_tree
219
247
        old_path = old_tree.id2path(file_id)
220
248
        new_path = new_tree.id2path(file_id)
221
 
        text_differ = diff.DiffText(old_tree, new_tree, diff_file)
 
249
        text_differ = diff.DiffText(old_tree, new_tree, diff_file,
 
250
            path_encoding=osutils.get_terminal_encoding())
222
251
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
223
252
        diff_file.seek(0)
224
253
        return patches.parse_patch(diff_file)
225
254
 
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):
 
255
    def prompt(self, message, choices, default):
 
256
        return ui.ui_factory.choose(message, choices, default=default)
 
257
 
 
258
    def prompt_bool(self, question, allow_editor=False):
239
259
        """Prompt the user with a yes/no question.
240
260
 
241
261
        This may be overridden by self.auto.  It may also *set* self.auto.  It
245
265
        """
246
266
        if self.auto:
247
267
            return True
248
 
        if long:
249
 
            prompt = ' [(y)es, (N)o, (f)inish, or (q)uit]'
 
268
        alternatives_chars = 'yn'
 
269
        alternatives = '&yes\n&No'
 
270
        if allow_editor:
 
271
            alternatives_chars += 'e'
 
272
            alternatives += '\n&edit manually'
 
273
        alternatives_chars += 'fq'
 
274
        alternatives += '\n&finish\n&quit'
 
275
        choice = self.prompt(question, alternatives, 1)
 
276
        if choice is None:
 
277
            # EOF.
 
278
            char = 'n'
250
279
        else:
251
 
            prompt = ' [yNfq?]'
252
 
        char = self.prompt(question + prompt)
 
280
            char = alternatives_chars[choice]
253
281
        if char == 'y':
254
282
            return True
 
283
        elif char == 'e' and allow_editor:
 
284
            raise UseEditor
255
285
        elif char == 'f':
256
286
            self.auto = True
257
287
            return True
258
 
        elif char == '?':
259
 
            return self.prompt_bool(question, long=True)
260
288
        if char == 'q':
261
289
            raise errors.UserAbort()
262
290
        else:
263
291
            return False
264
292
 
265
293
    def handle_modify_text(self, creator, file_id):
 
294
        """Handle modified text, by using hunk selection or file editing.
 
295
 
 
296
        :param creator: A ShelfCreator.
 
297
        :param file_id: The id of the file that was modified.
 
298
        :return: The number of changes.
 
299
        """
 
300
        work_tree_lines = self.work_tree.get_file_lines(file_id)
 
301
        try:
 
302
            lines, change_count = self._select_hunks(creator, file_id,
 
303
                                                     work_tree_lines)
 
304
        except UseEditor:
 
305
            lines, change_count = self._edit_file(file_id, work_tree_lines)
 
306
        if change_count != 0:
 
307
            creator.shelve_lines(file_id, lines)
 
308
        return change_count
 
309
 
 
310
    def _select_hunks(self, creator, file_id, work_tree_lines):
266
311
        """Provide diff hunk selection for modified text.
267
312
 
268
313
        If self.reporter.invert_diff is True, the diff is inverted so that
270
315
 
271
316
        :param creator: a ShelfCreator
272
317
        :param file_id: The id of the file to shelve.
 
318
        :param work_tree_lines: Line contents of the file in the working tree.
273
319
        :return: number of shelved hunks.
274
320
        """
275
321
        if self.reporter.invert_diff:
276
 
            target_lines = self.work_tree.get_file_lines(file_id)
 
322
            target_lines = work_tree_lines
277
323
        else:
278
324
            target_lines = self.target_tree.get_file_lines(file_id)
279
 
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
 
325
        textfile.check_text_lines(work_tree_lines)
280
326
        textfile.check_text_lines(target_lines)
281
327
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
282
328
        final_hunks = []
285
331
            self.diff_writer.write(parsed.get_header())
286
332
            for hunk in parsed.hunks:
287
333
                self.diff_writer.write(str(hunk))
288
 
                selected = self.prompt_bool(self.reporter.vocab['hunk'])
 
334
                selected = self.prompt_bool(self.reporter.vocab['hunk'],
 
335
                                            allow_editor=(self.change_editor
 
336
                                                          is not None))
289
337
                if not self.reporter.invert_diff:
290
338
                    selected = (not selected)
291
339
                if selected:
294
342
                else:
295
343
                    offset -= (hunk.mod_range - hunk.orig_range)
296
344
        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
345
        if self.reporter.invert_diff:
305
 
            return len(final_hunks)
306
 
        return len(parsed.hunks) - len(final_hunks)
 
346
            change_count = len(final_hunks)
 
347
        else:
 
348
            change_count = len(parsed.hunks) - len(final_hunks)
 
349
        patched = patches.iter_patched_from_hunks(target_lines,
 
350
                                                  final_hunks)
 
351
        lines = list(patched)
 
352
        return lines, change_count
 
353
 
 
354
    def _edit_file(self, file_id, work_tree_lines):
 
355
        """
 
356
        :param file_id: id of the file to edit.
 
357
        :param work_tree_lines: Line contents of the file in the working tree.
 
358
        :return: (lines, change_region_count), where lines is the new line
 
359
            content of the file, and change_region_count is the number of
 
360
            changed regions.
 
361
        """
 
362
        lines = osutils.split_lines(self.change_editor.edit_file(file_id))
 
363
        return lines, self._count_changed_regions(work_tree_lines, lines)
 
364
 
 
365
    @staticmethod
 
366
    def _count_changed_regions(old_lines, new_lines):
 
367
        matcher = patiencediff.PatienceSequenceMatcher(None, old_lines,
 
368
                                                       new_lines)
 
369
        blocks = matcher.get_matching_blocks()
 
370
        return len(blocks) - 2
307
371
 
308
372
 
309
373
class Unshelver(object):
310
374
    """Unshelve changes into a working tree."""
311
375
 
312
376
    @classmethod
313
 
    def from_args(klass, shelf_id=None, action='apply', directory='.'):
 
377
    def from_args(klass, shelf_id=None, action='apply', directory='.',
 
378
                  write_diff_to=None):
314
379
        """Create an unshelver from commandline arguments.
315
380
 
 
381
        The returned shelver will have a tree that is locked and should
 
382
        be unlocked.
 
383
 
316
384
        :param shelf_id: Integer id of the shelf, as a string.
317
385
        :param action: action to perform.  May be 'apply', 'dry-run',
318
 
            'delete'.
 
386
            'delete', 'preview'.
319
387
        :param directory: The directory to unshelve changes into.
 
388
        :param write_diff_to: See Unshelver.__init__().
320
389
        """
321
390
        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
 
391
        tree.lock_tree_write()
 
392
        try:
 
393
            manager = tree.get_shelf_manager()
 
394
            if shelf_id is not None:
 
395
                try:
 
396
                    shelf_id = int(shelf_id)
 
397
                except ValueError:
 
398
                    raise errors.InvalidShelfId(shelf_id)
 
399
            else:
 
400
                shelf_id = manager.last_shelf()
 
401
                if shelf_id is None:
 
402
                    raise errors.BzrCommandError(gettext('No changes are shelved.'))
 
403
            apply_changes = True
 
404
            delete_shelf = True
 
405
            read_shelf = True
 
406
            show_diff = False
 
407
            if action == 'dry-run':
 
408
                apply_changes = False
 
409
                delete_shelf = False
 
410
            elif action == 'preview':
 
411
                apply_changes = False
 
412
                delete_shelf = False
 
413
                show_diff = True
 
414
            elif action == 'delete-only':
 
415
                apply_changes = False
 
416
                read_shelf = False
 
417
            elif action == 'keep':
 
418
                apply_changes = True
 
419
                delete_shelf = False
 
420
        except:
 
421
            tree.unlock()
 
422
            raise
342
423
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
343
 
                     read_shelf)
 
424
                     read_shelf, show_diff, write_diff_to)
344
425
 
345
426
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
346
 
                 delete_shelf=True, read_shelf=True):
 
427
                 delete_shelf=True, read_shelf=True, show_diff=False,
 
428
                 write_diff_to=None):
347
429
        """Constructor.
348
430
 
349
431
        :param tree: The working tree to unshelve into.
353
435
            working tree.
354
436
        :param delete_shelf: If True, delete the changes from the shelf.
355
437
        :param read_shelf: If True, read the changes from the shelf.
 
438
        :param show_diff: If True, show the diff that would result from
 
439
            unshelving the changes.
 
440
        :param write_diff_to: A file-like object where the diff will be
 
441
            written to. If None, ui.ui_factory.make_output_stream() will
 
442
            be used.
356
443
        """
357
444
        self.tree = tree
358
445
        manager = tree.get_shelf_manager()
361
448
        self.apply_changes = apply_changes
362
449
        self.delete_shelf = delete_shelf
363
450
        self.read_shelf = read_shelf
 
451
        self.show_diff = show_diff
 
452
        self.write_diff_to = write_diff_to
364
453
 
365
454
    def run(self):
366
455
        """Perform the unshelving operation."""
367
 
        self.tree.lock_write()
 
456
        self.tree.lock_tree_write()
368
457
        cleanups = [self.tree.unlock]
369
458
        try:
370
459
            if self.read_shelf:
 
460
                trace.note(gettext('Using changes with id "%d".') % self.shelf_id)
371
461
                unshelver = self.manager.get_unshelver(self.shelf_id)
372
462
                cleanups.append(unshelver.finalize)
373
463
                if unshelver.message is not None:
374
 
                    trace.note('Message: %s' % unshelver.message)
 
464
                    trace.note(gettext('Message: %s') % unshelver.message)
375
465
                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()
 
466
                merger = unshelver.make_merger(None)
 
467
                merger.change_reporter = change_reporter
 
468
                if self.apply_changes:
 
469
                    merger.do_merge()
 
470
                elif self.show_diff:
 
471
                    self.write_diff(merger)
 
472
                else:
 
473
                    self.show_changes(merger)
386
474
            if self.delete_shelf:
387
475
                self.manager.delete_shelf(self.shelf_id)
 
476
                trace.note(gettext('Deleted changes with id "%d".') % self.shelf_id)
388
477
        finally:
389
478
            for cleanup in reversed(cleanups):
390
479
                cleanup()
391
480
 
 
481
    def write_diff(self, merger):
 
482
        """Write this operation's diff to self.write_diff_to."""
 
483
        tree_merger = merger.make_merger()
 
484
        tt = tree_merger.make_preview_transform()
 
485
        new_tree = tt.get_preview_tree()
 
486
        if self.write_diff_to is None:
 
487
            self.write_diff_to = ui.ui_factory.make_output_stream(encoding_type='exact')
 
488
        path_encoding = osutils.get_diff_header_encoding()
 
489
        diff.show_diff_trees(merger.this_tree, new_tree, self.write_diff_to,
 
490
            path_encoding=path_encoding)
 
491
        tt.finalize()
 
492
 
392
493
    def show_changes(self, merger):
393
494
        """Show the changes that this operation specifies."""
394
495
        tree_merger = merger.make_merger()