~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/shelf_ui.py

  • Committer: Ian Clatworthy
  • Date: 2009-09-09 15:30:59 UTC
  • mto: (4634.37.2 prepare-2.0)
  • mto: This revision was merged to the branch mainline in revision 4689.
  • Revision ID: ian.clatworthy@canonical.com-20090909153059-sb038agvd38ci2q8
more link fixes in the User Guide

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2008, 2009, 2010 Canonical Ltd
 
1
# Copyright (C) 2008 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
18
17
 
19
18
from cStringIO import StringIO
20
19
import shutil
28
27
    errors,
29
28
    osutils,
30
29
    patches,
31
 
    patiencediff,
32
30
    shelf,
33
31
    textfile,
34
32
    trace,
35
33
    ui,
36
34
    workingtree,
37
35
)
38
 
from bzrlib.i18n import gettext
39
 
 
40
 
class UseEditor(Exception):
41
 
    """Use an editor instead of selecting hunks."""
42
36
 
43
37
 
44
38
class ShelfReporter(object):
45
39
 
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"?')
 
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"?'
57
51
             }
58
52
 
59
53
    invert_diff = False
67
61
 
68
62
    def shelved_id(self, shelf_id):
69
63
        """Report the id changes were shelved to."""
70
 
        trace.note(gettext('Changes shelved with id "%d".') % shelf_id)
 
64
        trace.note('Changes shelved with id "%d".' % shelf_id)
71
65
 
72
66
    def changes_destroyed(self):
73
67
        """Report that changes were made without shelving."""
74
 
        trace.note(gettext('Selected changes destroyed.'))
 
68
        trace.note('Selected changes destroyed.')
75
69
 
76
70
    def selected_changes(self, transform):
77
71
        """Report the changes that were selected."""
78
 
        trace.note(gettext("Selected changes:"))
 
72
        trace.note("Selected changes:")
79
73
        changes = transform.iter_changes()
80
74
        delta.report_changes(changes, self.delta_reporter)
81
75
 
95
89
 
96
90
class ApplyReporter(ShelfReporter):
97
91
 
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"?'),
 
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"?',
108
102
             }
109
103
 
110
104
    invert_diff = True
149
143
        if reporter is None:
150
144
            reporter = ShelfReporter()
151
145
        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()
155
146
 
156
147
    @classmethod
157
148
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
158
 
                  message=None, directory=None, destroy=False):
 
149
                  message=None, directory='.', destroy=False):
159
150
        """Create a shelver from commandline arguments.
160
151
 
161
152
        The returned shelver wil have a work_tree that is locked and should
169
160
        :param destroy: Change the working tree without storing the shelved
170
161
            changes.
171
162
        """
172
 
        if directory is None:
173
 
            directory = u'.'
174
 
        elif file_list:
175
 
            file_list = [osutils.pathjoin(directory, f) for f in file_list]
176
163
        tree, path = workingtree.WorkingTree.open_containing(directory)
177
164
        # Ensure that tree is locked for the lifetime of target_tree, as
178
165
        # target tree may be reading from the same dirstate.
180
167
        try:
181
168
            target_tree = builtins._get_one_revision_tree('shelf2', revision,
182
169
                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:
 
170
            files = builtins.safe_relpath_files(tree, file_list)
 
171
        except:
187
172
            tree.unlock()
 
173
            raise
 
174
        return klass(tree, target_tree, diff_writer, all, all, files, message,
 
175
                     destroy)
188
176
 
189
177
    def run(self):
190
178
        """Interactively shelve the changes."""
223
211
            shutil.rmtree(self.tempdir)
224
212
            creator.finalize()
225
213
 
226
 
    def finalize(self):
227
 
        if self.change_editor is not None:
228
 
            self.change_editor.finish()
229
 
        self.work_tree.unlock()
230
 
 
231
 
 
232
214
    def get_parsed_patch(self, file_id, invert=False):
233
215
        """Return a parsed version of a file's patch.
234
216
 
246
228
            new_tree = self.work_tree
247
229
        old_path = old_tree.id2path(file_id)
248
230
        new_path = new_tree.id2path(file_id)
249
 
        text_differ = diff.DiffText(old_tree, new_tree, diff_file,
250
 
            path_encoding=osutils.get_terminal_encoding())
 
231
        text_differ = diff.DiffText(old_tree, new_tree, diff_file)
251
232
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
252
233
        diff_file.seek(0)
253
234
        return patches.parse_patch(diff_file)
254
235
 
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):
 
236
    def prompt(self, message):
 
237
        """Prompt the user for a character.
 
238
 
 
239
        :param message: The message to prompt a user with.
 
240
        :return: A character.
 
241
        """
 
242
        sys.stdout.write(message)
 
243
        char = osutils.getchar()
 
244
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
 
245
        sys.stdout.flush()
 
246
        return char
 
247
 
 
248
    def prompt_bool(self, question, long=False):
259
249
        """Prompt the user with a yes/no question.
260
250
 
261
251
        This may be overridden by self.auto.  It may also *set* self.auto.  It
265
255
        """
266
256
        if self.auto:
267
257
            return True
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'
 
258
        if long:
 
259
            prompt = ' [(y)es, (N)o, (f)inish, or (q)uit]'
279
260
        else:
280
 
            char = alternatives_chars[choice]
 
261
            prompt = ' [yNfq?]'
 
262
        char = self.prompt(question + prompt)
281
263
        if char == 'y':
282
264
            return True
283
 
        elif char == 'e' and allow_editor:
284
 
            raise UseEditor
285
265
        elif char == 'f':
286
266
            self.auto = True
287
267
            return True
 
268
        elif char == '?':
 
269
            return self.prompt_bool(question, long=True)
288
270
        if char == 'q':
289
271
            raise errors.UserAbort()
290
272
        else:
291
273
            return False
292
274
 
293
275
    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):
311
276
        """Provide diff hunk selection for modified text.
312
277
 
313
278
        If self.reporter.invert_diff is True, the diff is inverted so that
315
280
 
316
281
        :param creator: a ShelfCreator
317
282
        :param file_id: The id of the file to shelve.
318
 
        :param work_tree_lines: Line contents of the file in the working tree.
319
283
        :return: number of shelved hunks.
320
284
        """
321
285
        if self.reporter.invert_diff:
322
 
            target_lines = work_tree_lines
 
286
            target_lines = self.work_tree.get_file_lines(file_id)
323
287
        else:
324
288
            target_lines = self.target_tree.get_file_lines(file_id)
325
 
        textfile.check_text_lines(work_tree_lines)
 
289
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
326
290
        textfile.check_text_lines(target_lines)
327
291
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
328
292
        final_hunks = []
331
295
            self.diff_writer.write(parsed.get_header())
332
296
            for hunk in parsed.hunks:
333
297
                self.diff_writer.write(str(hunk))
334
 
                selected = self.prompt_bool(self.reporter.vocab['hunk'],
335
 
                                            allow_editor=(self.change_editor
336
 
                                                          is not None))
 
298
                selected = self.prompt_bool(self.reporter.vocab['hunk'])
337
299
                if not self.reporter.invert_diff:
338
300
                    selected = (not selected)
339
301
                if selected:
342
304
                else:
343
305
                    offset -= (hunk.mod_range - hunk.orig_range)
344
306
        sys.stdout.flush()
 
307
        if not self.reporter.invert_diff and (
 
308
            len(parsed.hunks) == len(final_hunks)):
 
309
            return 0
 
310
        if self.reporter.invert_diff and len(final_hunks) == 0:
 
311
            return 0
 
312
        patched = patches.iter_patched_from_hunks(target_lines, final_hunks)
 
313
        creator.shelve_lines(file_id, list(patched))
345
314
        if self.reporter.invert_diff:
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
 
315
            return len(final_hunks)
 
316
        return len(parsed.hunks) - len(final_hunks)
371
317
 
372
318
 
373
319
class Unshelver(object):
374
320
    """Unshelve changes into a working tree."""
375
321
 
376
322
    @classmethod
377
 
    def from_args(klass, shelf_id=None, action='apply', directory='.',
378
 
                  write_diff_to=None):
 
323
    def from_args(klass, shelf_id=None, action='apply', directory='.'):
379
324
        """Create an unshelver from commandline arguments.
380
325
 
381
 
        The returned shelver will have a tree that is locked and should
 
326
        The returned shelver wil have a tree that is locked and should
382
327
        be unlocked.
383
328
 
384
329
        :param shelf_id: Integer id of the shelf, as a string.
385
330
        :param action: action to perform.  May be 'apply', 'dry-run',
386
 
            'delete', 'preview'.
 
331
            'delete'.
387
332
        :param directory: The directory to unshelve changes into.
388
 
        :param write_diff_to: See Unshelver.__init__().
389
333
        """
390
334
        tree, path = workingtree.WorkingTree.open_containing(directory)
391
335
        tree.lock_tree_write()
399
343
            else:
400
344
                shelf_id = manager.last_shelf()
401
345
                if shelf_id is None:
402
 
                    raise errors.BzrCommandError(gettext('No changes are shelved.'))
 
346
                    raise errors.BzrCommandError('No changes are shelved.')
 
347
                trace.note('Unshelving changes with id "%d".' % shelf_id)
403
348
            apply_changes = True
404
349
            delete_shelf = True
405
350
            read_shelf = True
406
 
            show_diff = False
407
351
            if action == 'dry-run':
408
352
                apply_changes = False
409
353
                delete_shelf = False
410
 
            elif action == 'preview':
411
 
                apply_changes = False
412
 
                delete_shelf = False
413
 
                show_diff = True
414
 
            elif action == 'delete-only':
 
354
            if action == 'delete-only':
415
355
                apply_changes = False
416
356
                read_shelf = False
417
 
            elif action == 'keep':
418
 
                apply_changes = True
419
 
                delete_shelf = False
420
357
        except:
421
358
            tree.unlock()
422
359
            raise
423
360
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
424
 
                     read_shelf, show_diff, write_diff_to)
 
361
                     read_shelf)
425
362
 
426
363
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
427
 
                 delete_shelf=True, read_shelf=True, show_diff=False,
428
 
                 write_diff_to=None):
 
364
                 delete_shelf=True, read_shelf=True):
429
365
        """Constructor.
430
366
 
431
367
        :param tree: The working tree to unshelve into.
435
371
            working tree.
436
372
        :param delete_shelf: If True, delete the changes from the shelf.
437
373
        :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.
443
374
        """
444
375
        self.tree = tree
445
376
        manager = tree.get_shelf_manager()
448
379
        self.apply_changes = apply_changes
449
380
        self.delete_shelf = delete_shelf
450
381
        self.read_shelf = read_shelf
451
 
        self.show_diff = show_diff
452
 
        self.write_diff_to = write_diff_to
453
382
 
454
383
    def run(self):
455
384
        """Perform the unshelving operation."""
457
386
        cleanups = [self.tree.unlock]
458
387
        try:
459
388
            if self.read_shelf:
460
 
                trace.note(gettext('Using changes with id "%d".') % self.shelf_id)
461
389
                unshelver = self.manager.get_unshelver(self.shelf_id)
462
390
                cleanups.append(unshelver.finalize)
463
391
                if unshelver.message is not None:
464
 
                    trace.note(gettext('Message: %s') % unshelver.message)
 
392
                    trace.note('Message: %s' % unshelver.message)
465
393
                change_reporter = delta._ChangeReporter()
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)
 
394
                task = ui.ui_factory.nested_progress_bar()
 
395
                try:
 
396
                    merger = unshelver.make_merger(task)
 
397
                    merger.change_reporter = change_reporter
 
398
                    if self.apply_changes:
 
399
                        merger.do_merge()
 
400
                    else:
 
401
                        self.show_changes(merger)
 
402
                finally:
 
403
                    task.finished()
474
404
            if self.delete_shelf:
475
405
                self.manager.delete_shelf(self.shelf_id)
476
 
                trace.note(gettext('Deleted changes with id "%d".') % self.shelf_id)
477
406
        finally:
478
407
            for cleanup in reversed(cleanups):
479
408
                cleanup()
480
409
 
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
 
 
493
410
    def show_changes(self, merger):
494
411
        """Show the changes that this operation specifies."""
495
412
        tree_merger = merger.make_merger()