~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/shelf_ui.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-09 20:33:43 UTC
  • mto: (5029.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5030.
  • Revision ID: v.ladeuil+lp@free.fr-20100209203343-ktxx7t0xvptvjnt1
Move TestingPathFilteringServer to bzrlib.tests.test_server

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
 
40
45
    vocab = {'add file': 'Shelve adding file "%(path)s"?',
143
148
        if reporter is None:
144
149
            reporter = ShelfReporter()
145
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()
146
154
 
147
155
    @classmethod
148
156
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
149
157
                  message=None, directory='.', destroy=False):
150
158
        """Create a shelver from commandline arguments.
151
159
 
 
160
        The returned shelver wil have a work_tree that is locked and should
 
161
        be unlocked.
 
162
 
152
163
        :param revision: RevisionSpec of the revision to compare to.
153
164
        :param all: If True, shelve all changes without prompting.
154
165
        :param file_list: If supplied, only files in this list may be  shelved.
158
169
            changes.
159
170
        """
160
171
        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)
 
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()
166
183
 
167
184
    def run(self):
168
185
        """Interactively shelve the changes."""
201
218
            shutil.rmtree(self.tempdir)
202
219
            creator.finalize()
203
220
 
 
221
    def finalize(self):
 
222
        if self.change_editor is not None:
 
223
            self.change_editor.finish()
 
224
        self.work_tree.unlock()
 
225
 
 
226
 
204
227
    def get_parsed_patch(self, file_id, invert=False):
205
228
        """Return a parsed version of a file's patch.
206
229
 
229
252
        :param message: The message to prompt a user with.
230
253
        :return: A character.
231
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.")
232
261
        sys.stdout.write(message)
233
262
        char = osutils.getchar()
234
263
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
235
264
        sys.stdout.flush()
236
265
        return char
237
266
 
238
 
    def prompt_bool(self, question, long=False):
 
267
    def prompt_bool(self, question, long=False, allow_editor=False):
239
268
        """Prompt the user with a yes/no question.
240
269
 
241
270
        This may be overridden by self.auto.  It may also *set* self.auto.  It
245
274
        """
246
275
        if self.auto:
247
276
            return True
 
277
        editor_string = ''
248
278
        if long:
249
 
            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
250
282
        else:
251
 
            prompt = ' [yNfq?]'
 
283
            if allow_editor:
 
284
                editor_string = 'e'
 
285
            prompt = ' [yN%sfq?]' % editor_string
252
286
        char = self.prompt(question + prompt)
253
287
        if char == 'y':
254
288
            return True
 
289
        elif char == 'e' and allow_editor:
 
290
            raise UseEditor
255
291
        elif char == 'f':
256
292
            self.auto = True
257
293
            return True
263
299
            return False
264
300
 
265
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):
266
319
        """Provide diff hunk selection for modified text.
267
320
 
268
321
        If self.reporter.invert_diff is True, the diff is inverted so that
270
323
 
271
324
        :param creator: a ShelfCreator
272
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.
273
327
        :return: number of shelved hunks.
274
328
        """
275
329
        if self.reporter.invert_diff:
276
 
            target_lines = self.work_tree.get_file_lines(file_id)
 
330
            target_lines = work_tree_lines
277
331
        else:
278
332
            target_lines = self.target_tree.get_file_lines(file_id)
279
 
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
 
333
        textfile.check_text_lines(work_tree_lines)
280
334
        textfile.check_text_lines(target_lines)
281
335
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
282
336
        final_hunks = []
285
339
            self.diff_writer.write(parsed.get_header())
286
340
            for hunk in parsed.hunks:
287
341
                self.diff_writer.write(str(hunk))
288
 
                selected = self.prompt_bool(self.reporter.vocab['hunk'])
 
342
                selected = self.prompt_bool(self.reporter.vocab['hunk'],
 
343
                                            allow_editor=(self.change_editor
 
344
                                                          is not None))
289
345
                if not self.reporter.invert_diff:
290
346
                    selected = (not selected)
291
347
                if selected:
294
350
                else:
295
351
                    offset -= (hunk.mod_range - hunk.orig_range)
296
352
        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
353
        if self.reporter.invert_diff:
305
 
            return len(final_hunks)
306
 
        return len(parsed.hunks) - len(final_hunks)
 
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
307
379
 
308
380
 
309
381
class Unshelver(object):
310
382
    """Unshelve changes into a working tree."""
311
383
 
312
384
    @classmethod
313
 
    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):
314
387
        """Create an unshelver from commandline arguments.
315
388
 
 
389
        The returned shelver will have a tree that is locked and should
 
390
        be unlocked.
 
391
 
316
392
        :param shelf_id: Integer id of the shelf, as a string.
317
393
        :param action: action to perform.  May be 'apply', 'dry-run',
318
 
            'delete'.
 
394
            'delete', 'preview'.
319
395
        :param directory: The directory to unshelve changes into.
 
396
        :param write_diff_to: See Unshelver.__init__().
320
397
        """
321
398
        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
 
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
342
431
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
343
 
                     read_shelf)
 
432
                     read_shelf, show_diff, write_diff_to)
344
433
 
345
434
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
346
 
                 delete_shelf=True, read_shelf=True):
 
435
                 delete_shelf=True, read_shelf=True, show_diff=False,
 
436
                 write_diff_to=None):
347
437
        """Constructor.
348
438
 
349
439
        :param tree: The working tree to unshelve into.
353
443
            working tree.
354
444
        :param delete_shelf: If True, delete the changes from the shelf.
355
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.
356
451
        """
357
452
        self.tree = tree
358
453
        manager = tree.get_shelf_manager()
361
456
        self.apply_changes = apply_changes
362
457
        self.delete_shelf = delete_shelf
363
458
        self.read_shelf = read_shelf
 
459
        self.show_diff = show_diff
 
460
        self.write_diff_to = write_diff_to
364
461
 
365
462
    def run(self):
366
463
        """Perform the unshelving operation."""
367
 
        self.tree.lock_write()
 
464
        self.tree.lock_tree_write()
368
465
        cleanups = [self.tree.unlock]
369
466
        try:
370
467
            if self.read_shelf:
 
468
                trace.note('Using changes with id "%d".' % self.shelf_id)
371
469
                unshelver = self.manager.get_unshelver(self.shelf_id)
372
470
                cleanups.append(unshelver.finalize)
373
471
                if unshelver.message is not None:
379
477
                    merger.change_reporter = change_reporter
380
478
                    if self.apply_changes:
381
479
                        merger.do_merge()
 
480
                    elif self.show_diff:
 
481
                        self.write_diff(merger)
382
482
                    else:
383
483
                        self.show_changes(merger)
384
484
                finally:
385
485
                    task.finished()
386
486
            if self.delete_shelf:
387
487
                self.manager.delete_shelf(self.shelf_id)
 
488
                trace.note('Deleted changes with id "%d".' % self.shelf_id)
388
489
        finally:
389
490
            for cleanup in reversed(cleanups):
390
491
                cleanup()
391
492
 
 
493
    def write_diff(self, merger):
 
494
        """Write this operation's diff to self.write_diff_to."""
 
495
        tree_merger = merger.make_merger()
 
496
        tt = tree_merger.make_preview_transform()
 
497
        new_tree = tt.get_preview_tree()
 
498
        if self.write_diff_to is None:
 
499
            self.write_diff_to = ui.ui_factory.make_output_stream()
 
500
        diff.show_diff_trees(merger.this_tree, new_tree, self.write_diff_to)
 
501
        tt.finalize()
 
502
 
392
503
    def show_changes(self, merger):
393
504
        """Show the changes that this operation specifies."""
394
505
        tree_merger = merger.make_merger()