/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: Robert Collins
  • Date: 2010-05-06 11:08:10 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506110810-h3j07fh5gmw54s25
Cleaner matcher matching revised unlocking protocol.

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
27
27
    errors,
28
28
    osutils,
29
29
    patches,
 
30
    patiencediff,
30
31
    shelf,
31
32
    textfile,
32
33
    trace,
35
36
)
36
37
 
37
38
 
 
39
class UseEditor(Exception):
 
40
    """Use an editor instead of selecting hunks."""
 
41
 
 
42
 
38
43
class ShelfReporter(object):
39
44
 
 
45
    vocab = {'add file': 'Shelve adding file "%(path)s"?',
 
46
             'binary': 'Shelve binary changes?',
 
47
             'change kind': 'Shelve changing "%s" from %(other)s'
 
48
             ' to %(this)s?',
 
49
             'delete file': 'Shelve removing file "%(path)s"?',
 
50
             'final': 'Shelve %d change(s)?',
 
51
             'hunk': 'Shelve?',
 
52
             'modify target': 'Shelve changing target of'
 
53
             ' "%(path)s" from "%(other)s" to "%(this)s"?',
 
54
             'rename': 'Shelve renaming "%(other)s" =>'
 
55
                        ' "%(this)s"?'
 
56
             }
 
57
 
 
58
    invert_diff = False
 
59
 
40
60
    def __init__(self):
41
61
        self.delta_reporter = delta._ChangeReporter()
42
62
 
43
63
    def no_changes(self):
 
64
        """Report that no changes were selected to apply."""
44
65
        trace.warning('No changes to shelve.')
45
66
 
46
67
    def shelved_id(self, shelf_id):
 
68
        """Report the id changes were shelved to."""
47
69
        trace.note('Changes shelved with id "%d".' % shelf_id)
48
70
 
 
71
    def changes_destroyed(self):
 
72
        """Report that changes were made without shelving."""
 
73
        trace.note('Selected changes destroyed.')
 
74
 
49
75
    def selected_changes(self, transform):
 
76
        """Report the changes that were selected."""
50
77
        trace.note("Selected changes:")
51
78
        changes = transform.iter_changes()
52
79
        delta.report_changes(changes, self.delta_reporter)
53
80
 
 
81
    def prompt_change(self, change):
 
82
        """Determine the prompt for a change to apply."""
 
83
        if change[0] == 'rename':
 
84
            vals = {'this': change[3], 'other': change[2]}
 
85
        elif change[0] == 'change kind':
 
86
            vals = {'path': change[4], 'other': change[2], 'this': change[3]}
 
87
        elif change[0] == 'modify target':
 
88
            vals = {'path': change[2], 'other': change[3], 'this': change[4]}
 
89
        else:
 
90
            vals = {'path': change[3]}
 
91
        prompt = self.vocab[change[0]] % vals
 
92
        return prompt
 
93
 
 
94
 
 
95
class ApplyReporter(ShelfReporter):
 
96
 
 
97
    vocab = {'add file': 'Delete file "%(path)s"?',
 
98
             'binary': 'Apply binary changes?',
 
99
             'change kind': 'Change "%(path)s" from %(this)s'
 
100
             ' to %(other)s?',
 
101
             'delete file': 'Add file "%(path)s"?',
 
102
             'final': 'Apply %d change(s)?',
 
103
             'hunk': 'Apply change?',
 
104
             'modify target': 'Change target of'
 
105
             ' "%(path)s" from "%(this)s" to "%(other)s"?',
 
106
             'rename': 'Rename "%(this)s" => "%(other)s"?',
 
107
             }
 
108
 
 
109
    invert_diff = True
 
110
 
 
111
    def changes_destroyed(self):
 
112
        pass
 
113
 
54
114
 
55
115
class Shelver(object):
56
116
    """Interactively shelve the changes in a working tree."""
70
130
        :param destroy: Change the working tree without storing the shelved
71
131
            changes.
72
132
        :param manager: The shelf manager to use.
 
133
        :param reporter: Object for reporting changes to user.
73
134
        """
74
135
        self.work_tree = work_tree
75
136
        self.target_tree = target_tree
87
148
        if reporter is None:
88
149
            reporter = ShelfReporter()
89
150
        self.reporter = reporter
 
151
        config = self.work_tree.branch.get_config()
 
152
        self.change_editor = config.get_change_editor(target_tree, work_tree)
 
153
        self.work_tree.lock_tree_write()
90
154
 
91
155
    @classmethod
92
156
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
93
157
                  message=None, directory='.', destroy=False):
94
158
        """Create a shelver from commandline arguments.
95
159
 
 
160
        The returned shelver wil have a work_tree that is locked and should
 
161
        be unlocked.
 
162
 
96
163
        :param revision: RevisionSpec of the revision to compare to.
97
164
        :param all: If True, shelve all changes without prompting.
98
165
        :param file_list: If supplied, only files in this list may be  shelved.
102
169
            changes.
103
170
        """
104
171
        tree, path = workingtree.WorkingTree.open_containing(directory)
105
 
        target_tree = builtins._get_one_revision_tree('shelf2', revision,
106
 
            tree.branch, tree)
107
 
        files = builtins.safe_relpath_files(tree, file_list)
108
 
        return klass(tree, target_tree, diff_writer, all, all, files, message,
109
 
                     destroy)
 
172
        # Ensure that tree is locked for the lifetime of target_tree, as
 
173
        # target tree may be reading from the same dirstate.
 
174
        tree.lock_tree_write()
 
175
        try:
 
176
            target_tree = builtins._get_one_revision_tree('shelf2', revision,
 
177
                tree.branch, tree)
 
178
            files = builtins.safe_relpath_files(tree, file_list)
 
179
            return klass(tree, target_tree, diff_writer, all, all, files,
 
180
                         message, destroy)
 
181
        finally:
 
182
            tree.unlock()
110
183
 
111
184
    def run(self):
112
185
        """Interactively shelve the changes."""
121
194
                        changes_shelved += self.handle_modify_text(creator,
122
195
                                                                   change[1])
123
196
                    except errors.BinaryFile:
124
 
                        if self.prompt_bool('Shelve binary changes?'):
 
197
                        if self.prompt_bool(self.reporter.vocab['binary']):
125
198
                            changes_shelved += 1
126
199
                            creator.shelve_content_change(change[1])
127
 
                if change[0] == 'add file':
128
 
                    if self.prompt_bool('Shelve adding file "%s"?'
129
 
                                        % change[3]):
130
 
                        creator.shelve_creation(change[1])
131
 
                        changes_shelved += 1
132
 
                if change[0] == 'delete file':
133
 
                    if self.prompt_bool('Shelve removing file "%s"?'
134
 
                                        % change[3]):
135
 
                        creator.shelve_deletion(change[1])
136
 
                        changes_shelved += 1
137
 
                if change[0] == 'change kind':
138
 
                    if self.prompt_bool('Shelve changing "%s" from %s to %s? '
139
 
                                        % (change[4], change[2], change[3])):
140
 
                        creator.shelve_content_change(change[1])
141
 
                        changes_shelved += 1
142
 
                if change[0] == 'rename':
143
 
                    if self.prompt_bool('Shelve renaming "%s" => "%s"?' %
144
 
                                   change[2:]):
145
 
                        creator.shelve_rename(change[1])
146
 
                        changes_shelved += 1
147
 
                if change[0] == 'modify target':
148
 
                    if self.prompt_bool('Shelve changing target of "%s" '
149
 
                            'from "%s" to "%s"?' % change[2:]):
150
 
                        creator.shelve_modify_target(change[1])
 
200
                else:
 
201
                    if self.prompt_bool(self.reporter.prompt_change(change)):
 
202
                        creator.shelve_change(change)
151
203
                        changes_shelved += 1
152
204
            if changes_shelved > 0:
153
205
                self.reporter.selected_changes(creator.work_transform)
154
206
                if (self.auto_apply or self.prompt_bool(
155
 
                    'Shelve %d change(s)?' % changes_shelved)):
 
207
                    self.reporter.vocab['final'] % changes_shelved)):
156
208
                    if self.destroy:
157
209
                        creator.transform()
158
 
                        trace.note('Selected changes destroyed.')
 
210
                        self.reporter.changes_destroyed()
159
211
                    else:
160
212
                        shelf_id = self.manager.shelve_changes(creator,
161
213
                                                               self.message)
166
218
            shutil.rmtree(self.tempdir)
167
219
            creator.finalize()
168
220
 
169
 
    def get_parsed_patch(self, file_id):
 
221
    def finalize(self):
 
222
        if self.change_editor is not None:
 
223
            self.change_editor.finish()
 
224
        self.work_tree.unlock()
 
225
 
 
226
 
 
227
    def get_parsed_patch(self, file_id, invert=False):
170
228
        """Return a parsed version of a file's patch.
171
229
 
172
230
        :param file_id: The id of the file to generate a patch for.
 
231
        :param invert: If True, provide an inverted patch (insertions displayed
 
232
            as removals, removals displayed as insertions).
173
233
        :return: A patches.Patch.
174
234
        """
175
 
        old_path = self.target_tree.id2path(file_id)
176
 
        new_path = self.work_tree.id2path(file_id)
177
235
        diff_file = StringIO()
178
 
        text_differ = diff.DiffText(self.target_tree, self.work_tree,
179
 
                                    diff_file)
 
236
        if invert:
 
237
            old_tree = self.work_tree
 
238
            new_tree = self.target_tree
 
239
        else:
 
240
            old_tree = self.target_tree
 
241
            new_tree = self.work_tree
 
242
        old_path = old_tree.id2path(file_id)
 
243
        new_path = new_tree.id2path(file_id)
 
244
        text_differ = diff.DiffText(old_tree, new_tree, diff_file)
180
245
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
181
246
        diff_file.seek(0)
182
247
        return patches.parse_patch(diff_file)
187
252
        :param message: The message to prompt a user with.
188
253
        :return: A character.
189
254
        """
 
255
        if not sys.stdin.isatty():
 
256
            # Since there is no controlling terminal we will hang when trying
 
257
            # to prompt the user, better abort now.  See
 
258
            # https://code.launchpad.net/~bialix/bzr/shelve-no-tty/+merge/14905
 
259
            # for more context.
 
260
            raise errors.BzrError("You need a controlling terminal.")
190
261
        sys.stdout.write(message)
191
262
        char = osutils.getchar()
192
263
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
193
264
        sys.stdout.flush()
194
265
        return char
195
266
 
196
 
    def prompt_bool(self, question, long=False):
 
267
    def prompt_bool(self, question, long=False, allow_editor=False):
197
268
        """Prompt the user with a yes/no question.
198
269
 
199
270
        This may be overridden by self.auto.  It may also *set* self.auto.  It
203
274
        """
204
275
        if self.auto:
205
276
            return True
 
277
        editor_string = ''
206
278
        if long:
207
 
            prompt = ' [(y)es, (N)o, (f)inish, or (q)uit]'
 
279
            if allow_editor:
 
280
                editor_string = '(E)dit manually, '
 
281
            prompt = ' [(y)es, (N)o, %s(f)inish, or (q)uit]' % editor_string
208
282
        else:
209
 
            prompt = ' [yNfq?]'
 
283
            if allow_editor:
 
284
                editor_string = 'e'
 
285
            prompt = ' [yN%sfq?]' % editor_string
210
286
        char = self.prompt(question + prompt)
211
287
        if char == 'y':
212
288
            return True
 
289
        elif char == 'e' and allow_editor:
 
290
            raise UseEditor
213
291
        elif char == 'f':
214
292
            self.auto = True
215
293
            return True
221
299
            return False
222
300
 
223
301
    def handle_modify_text(self, creator, file_id):
 
302
        """Handle modified text, by using hunk selection or file editing.
 
303
 
 
304
        :param creator: A ShelfCreator.
 
305
        :param file_id: The id of the file that was modified.
 
306
        :return: The number of changes.
 
307
        """
 
308
        work_tree_lines = self.work_tree.get_file_lines(file_id)
 
309
        try:
 
310
            lines, change_count = self._select_hunks(creator, file_id,
 
311
                                                     work_tree_lines)
 
312
        except UseEditor:
 
313
            lines, change_count = self._edit_file(file_id, work_tree_lines)
 
314
        if change_count != 0:
 
315
            creator.shelve_lines(file_id, lines)
 
316
        return change_count
 
317
 
 
318
    def _select_hunks(self, creator, file_id, work_tree_lines):
224
319
        """Provide diff hunk selection for modified text.
225
320
 
 
321
        If self.reporter.invert_diff is True, the diff is inverted so that
 
322
        insertions are displayed as removals and vice versa.
 
323
 
226
324
        :param creator: a ShelfCreator
227
325
        :param file_id: The id of the file to shelve.
 
326
        :param work_tree_lines: Line contents of the file in the working tree.
228
327
        :return: number of shelved hunks.
229
328
        """
230
 
        target_lines = self.target_tree.get_file_lines(file_id)
231
 
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
 
329
        if self.reporter.invert_diff:
 
330
            target_lines = work_tree_lines
 
331
        else:
 
332
            target_lines = self.target_tree.get_file_lines(file_id)
 
333
        textfile.check_text_lines(work_tree_lines)
232
334
        textfile.check_text_lines(target_lines)
233
 
        parsed = self.get_parsed_patch(file_id)
 
335
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
234
336
        final_hunks = []
235
337
        if not self.auto:
236
338
            offset = 0
237
339
            self.diff_writer.write(parsed.get_header())
238
340
            for hunk in parsed.hunks:
239
341
                self.diff_writer.write(str(hunk))
240
 
                if not self.prompt_bool('Shelve?'):
 
342
                selected = self.prompt_bool(self.reporter.vocab['hunk'],
 
343
                                            allow_editor=(self.change_editor
 
344
                                                          is not None))
 
345
                if not self.reporter.invert_diff:
 
346
                    selected = (not selected)
 
347
                if selected:
241
348
                    hunk.mod_pos += offset
242
349
                    final_hunks.append(hunk)
243
350
                else:
244
351
                    offset -= (hunk.mod_range - hunk.orig_range)
245
352
        sys.stdout.flush()
246
 
        if len(parsed.hunks) == len(final_hunks):
247
 
            return 0
248
 
        patched = patches.iter_patched_from_hunks(target_lines, final_hunks)
249
 
        creator.shelve_lines(file_id, list(patched))
250
 
        return len(parsed.hunks) - len(final_hunks)
 
353
        if self.reporter.invert_diff:
 
354
            change_count = len(final_hunks)
 
355
        else:
 
356
            change_count = len(parsed.hunks) - len(final_hunks)
 
357
        patched = patches.iter_patched_from_hunks(target_lines,
 
358
                                                  final_hunks)
 
359
        lines = list(patched)
 
360
        return lines, change_count
 
361
 
 
362
    def _edit_file(self, file_id, work_tree_lines):
 
363
        """
 
364
        :param file_id: id of the file to edit.
 
365
        :param work_tree_lines: Line contents of the file in the working tree.
 
366
        :return: (lines, change_region_count), where lines is the new line
 
367
            content of the file, and change_region_count is the number of
 
368
            changed regions.
 
369
        """
 
370
        lines = osutils.split_lines(self.change_editor.edit_file(file_id))
 
371
        return lines, self._count_changed_regions(work_tree_lines, lines)
 
372
 
 
373
    @staticmethod
 
374
    def _count_changed_regions(old_lines, new_lines):
 
375
        matcher = patiencediff.PatienceSequenceMatcher(None, old_lines,
 
376
                                                       new_lines)
 
377
        blocks = matcher.get_matching_blocks()
 
378
        return len(blocks) - 2
251
379
 
252
380
 
253
381
class Unshelver(object):
254
382
    """Unshelve changes into a working tree."""
255
383
 
256
384
    @classmethod
257
 
    def from_args(klass, shelf_id=None, action='apply', directory='.'):
 
385
    def from_args(klass, shelf_id=None, action='apply', directory='.',
 
386
                  write_diff_to=None):
258
387
        """Create an unshelver from commandline arguments.
259
388
 
 
389
        The returned shelver will have a tree that is locked and should
 
390
        be unlocked.
 
391
 
260
392
        :param shelf_id: Integer id of the shelf, as a string.
261
393
        :param action: action to perform.  May be 'apply', 'dry-run',
262
 
            'delete'.
 
394
            'delete', 'preview'.
263
395
        :param directory: The directory to unshelve changes into.
 
396
        :param write_diff_to: See Unshelver.__init__().
264
397
        """
265
398
        tree, path = workingtree.WorkingTree.open_containing(directory)
266
 
        manager = tree.get_shelf_manager()
267
 
        if shelf_id is not None:
268
 
            try:
269
 
                shelf_id = int(shelf_id)
270
 
            except ValueError:
271
 
                raise errors.InvalidShelfId(shelf_id)
272
 
        else:
273
 
            shelf_id = manager.last_shelf()
274
 
            if shelf_id is None:
275
 
                raise errors.BzrCommandError('No changes are shelved.')
276
 
            trace.note('Unshelving changes with id "%d".' % shelf_id)
277
 
        apply_changes = True
278
 
        delete_shelf = True
279
 
        read_shelf = True
280
 
        if action == 'dry-run':
281
 
            apply_changes = False
282
 
            delete_shelf = False
283
 
        if action == 'delete-only':
284
 
            apply_changes = False
285
 
            read_shelf = False
 
399
        tree.lock_tree_write()
 
400
        try:
 
401
            manager = tree.get_shelf_manager()
 
402
            if shelf_id is not None:
 
403
                try:
 
404
                    shelf_id = int(shelf_id)
 
405
                except ValueError:
 
406
                    raise errors.InvalidShelfId(shelf_id)
 
407
            else:
 
408
                shelf_id = manager.last_shelf()
 
409
                if shelf_id is None:
 
410
                    raise errors.BzrCommandError('No changes are shelved.')
 
411
            apply_changes = True
 
412
            delete_shelf = True
 
413
            read_shelf = True
 
414
            show_diff = False
 
415
            if action == 'dry-run':
 
416
                apply_changes = False
 
417
                delete_shelf = False
 
418
            elif action == 'preview':
 
419
                apply_changes = False
 
420
                delete_shelf = False
 
421
                show_diff = True
 
422
            elif action == 'delete-only':
 
423
                apply_changes = False
 
424
                read_shelf = False
 
425
            elif action == 'keep':
 
426
                apply_changes = True
 
427
                delete_shelf = False
 
428
        except:
 
429
            tree.unlock()
 
430
            raise
286
431
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
287
 
                     read_shelf)
 
432
                     read_shelf, show_diff, write_diff_to)
288
433
 
289
434
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
290
 
                 delete_shelf=True, read_shelf=True):
 
435
                 delete_shelf=True, read_shelf=True, show_diff=False,
 
436
                 write_diff_to=None):
291
437
        """Constructor.
292
438
 
293
439
        :param tree: The working tree to unshelve into.
297
443
            working tree.
298
444
        :param delete_shelf: If True, delete the changes from the shelf.
299
445
        :param read_shelf: If True, read the changes from the shelf.
 
446
        :param show_diff: If True, show the diff that would result from
 
447
            unshelving the changes.
 
448
        :param write_diff_to: A file-like object where the diff will be
 
449
            written to. If None, ui.ui_factory.make_output_stream() will
 
450
            be used.
300
451
        """
301
452
        self.tree = tree
302
453
        manager = tree.get_shelf_manager()
305
456
        self.apply_changes = apply_changes
306
457
        self.delete_shelf = delete_shelf
307
458
        self.read_shelf = read_shelf
 
459
        self.show_diff = show_diff
 
460
        self.write_diff_to = write_diff_to
308
461
 
309
462
    def run(self):
310
463
        """Perform the unshelving operation."""
311
 
        self.tree.lock_write()
 
464
        self.tree.lock_tree_write()
312
465
        cleanups = [self.tree.unlock]
313
466
        try:
314
467
            if self.read_shelf:
 
468
                trace.note('Using changes with id "%d".' % self.shelf_id)
315
469
                unshelver = self.manager.get_unshelver(self.shelf_id)
316
470
                cleanups.append(unshelver.finalize)
317
471
                if unshelver.message is not None:
318
472
                    trace.note('Message: %s' % unshelver.message)
319
473
                change_reporter = delta._ChangeReporter()
320
 
                task = ui.ui_factory.nested_progress_bar()
321
 
                try:
322
 
                    merger = unshelver.make_merger(task)
323
 
                    merger.change_reporter = change_reporter
324
 
                    if self.apply_changes:
325
 
                        merger.do_merge()
326
 
                    else:
327
 
                        self.show_changes(merger)
328
 
                finally:
329
 
                    task.finished()
 
474
                merger = unshelver.make_merger(None)
 
475
                merger.change_reporter = change_reporter
 
476
                if self.apply_changes:
 
477
                    merger.do_merge()
 
478
                elif self.show_diff:
 
479
                    self.write_diff(merger)
 
480
                else:
 
481
                    self.show_changes(merger)
330
482
            if self.delete_shelf:
331
483
                self.manager.delete_shelf(self.shelf_id)
 
484
                trace.note('Deleted changes with id "%d".' % self.shelf_id)
332
485
        finally:
333
486
            for cleanup in reversed(cleanups):
334
487
                cleanup()
335
488
 
 
489
    def write_diff(self, merger):
 
490
        """Write this operation's diff to self.write_diff_to."""
 
491
        tree_merger = merger.make_merger()
 
492
        tt = tree_merger.make_preview_transform()
 
493
        new_tree = tt.get_preview_tree()
 
494
        if self.write_diff_to is None:
 
495
            self.write_diff_to = ui.ui_factory.make_output_stream()
 
496
        diff.show_diff_trees(merger.this_tree, new_tree, self.write_diff_to)
 
497
        tt.finalize()
 
498
 
336
499
    def show_changes(self, merger):
337
500
        """Show the changes that this operation specifies."""
338
501
        tree_merger = merger.make_merger()