~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/shelf_ui.py

  • Committer: Martin Pool
  • Date: 2009-08-14 12:08:08 UTC
  • mto: This revision was merged to the branch mainline in revision 4614.
  • Revision ID: mbp@sourcefrog.net-20090814120808-4gvx7fhg44z29cj3
Use platform(aliased=1)

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
27
27
    errors,
28
28
    osutils,
29
29
    patches,
30
 
    patiencediff,
31
30
    shelf,
32
31
    textfile,
33
32
    trace,
36
35
)
37
36
 
38
37
 
39
 
class UseEditor(Exception):
40
 
    """Use an editor instead of selecting hunks."""
41
 
 
42
 
 
43
38
class ShelfReporter(object):
44
39
 
45
40
    vocab = {'add file': 'Shelve adding file "%(path)s"?',
148
143
        if reporter is None:
149
144
            reporter = ShelfReporter()
150
145
        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()
154
146
 
155
147
    @classmethod
156
148
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
157
149
                  message=None, directory='.', destroy=False):
158
150
        """Create a shelver from commandline arguments.
159
151
 
160
 
        The returned shelver wil have a work_tree that is locked and should
161
 
        be unlocked.
162
 
 
163
152
        :param revision: RevisionSpec of the revision to compare to.
164
153
        :param all: If True, shelve all changes without prompting.
165
154
        :param file_list: If supplied, only files in this list may be  shelved.
169
158
            changes.
170
159
        """
171
160
        tree, path = workingtree.WorkingTree.open_containing(directory)
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 = tree.safe_relpath_files(file_list)
179
 
            return klass(tree, target_tree, diff_writer, all, all, files,
180
 
                         message, destroy)
181
 
        finally:
182
 
            tree.unlock()
 
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)
183
166
 
184
167
    def run(self):
185
168
        """Interactively shelve the changes."""
218
201
            shutil.rmtree(self.tempdir)
219
202
            creator.finalize()
220
203
 
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
204
    def get_parsed_patch(self, file_id, invert=False):
228
205
        """Return a parsed version of a file's patch.
229
206
 
241
218
            new_tree = self.work_tree
242
219
        old_path = old_tree.id2path(file_id)
243
220
        new_path = new_tree.id2path(file_id)
244
 
        text_differ = diff.DiffText(old_tree, new_tree, diff_file,
245
 
            path_encoding=osutils.get_terminal_encoding())
 
221
        text_differ = diff.DiffText(old_tree, new_tree, diff_file)
246
222
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
247
223
        diff_file.seek(0)
248
224
        return patches.parse_patch(diff_file)
253
229
        :param message: The message to prompt a user with.
254
230
        :return: A character.
255
231
        """
256
 
        if not sys.stdin.isatty():
257
 
            # Since there is no controlling terminal we will hang when trying
258
 
            # to prompt the user, better abort now.  See
259
 
            # https://code.launchpad.net/~bialix/bzr/shelve-no-tty/+merge/14905
260
 
            # for more context.
261
 
            raise errors.BzrError("You need a controlling terminal.")
262
232
        sys.stdout.write(message)
263
233
        char = osutils.getchar()
264
234
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
265
235
        sys.stdout.flush()
266
236
        return char
267
237
 
268
 
    def prompt_bool(self, question, long=False, allow_editor=False):
 
238
    def prompt_bool(self, question, long=False):
269
239
        """Prompt the user with a yes/no question.
270
240
 
271
241
        This may be overridden by self.auto.  It may also *set* self.auto.  It
275
245
        """
276
246
        if self.auto:
277
247
            return True
278
 
        editor_string = ''
279
248
        if long:
280
 
            if allow_editor:
281
 
                editor_string = '(E)dit manually, '
282
 
            prompt = ' [(y)es, (N)o, %s(f)inish, or (q)uit]' % editor_string
 
249
            prompt = ' [(y)es, (N)o, (f)inish, or (q)uit]'
283
250
        else:
284
 
            if allow_editor:
285
 
                editor_string = 'e'
286
 
            prompt = ' [yN%sfq?]' % editor_string
 
251
            prompt = ' [yNfq?]'
287
252
        char = self.prompt(question + prompt)
288
253
        if char == 'y':
289
254
            return True
290
 
        elif char == 'e' and allow_editor:
291
 
            raise UseEditor
292
255
        elif char == 'f':
293
256
            self.auto = True
294
257
            return True
300
263
            return False
301
264
 
302
265
    def handle_modify_text(self, creator, file_id):
303
 
        """Handle modified text, by using hunk selection or file editing.
304
 
 
305
 
        :param creator: A ShelfCreator.
306
 
        :param file_id: The id of the file that was modified.
307
 
        :return: The number of changes.
308
 
        """
309
 
        work_tree_lines = self.work_tree.get_file_lines(file_id)
310
 
        try:
311
 
            lines, change_count = self._select_hunks(creator, file_id,
312
 
                                                     work_tree_lines)
313
 
        except UseEditor:
314
 
            lines, change_count = self._edit_file(file_id, work_tree_lines)
315
 
        if change_count != 0:
316
 
            creator.shelve_lines(file_id, lines)
317
 
        return change_count
318
 
 
319
 
    def _select_hunks(self, creator, file_id, work_tree_lines):
320
266
        """Provide diff hunk selection for modified text.
321
267
 
322
268
        If self.reporter.invert_diff is True, the diff is inverted so that
324
270
 
325
271
        :param creator: a ShelfCreator
326
272
        :param file_id: The id of the file to shelve.
327
 
        :param work_tree_lines: Line contents of the file in the working tree.
328
273
        :return: number of shelved hunks.
329
274
        """
330
275
        if self.reporter.invert_diff:
331
 
            target_lines = work_tree_lines
 
276
            target_lines = self.work_tree.get_file_lines(file_id)
332
277
        else:
333
278
            target_lines = self.target_tree.get_file_lines(file_id)
334
 
        textfile.check_text_lines(work_tree_lines)
 
279
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
335
280
        textfile.check_text_lines(target_lines)
336
281
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
337
282
        final_hunks = []
340
285
            self.diff_writer.write(parsed.get_header())
341
286
            for hunk in parsed.hunks:
342
287
                self.diff_writer.write(str(hunk))
343
 
                selected = self.prompt_bool(self.reporter.vocab['hunk'],
344
 
                                            allow_editor=(self.change_editor
345
 
                                                          is not None))
 
288
                selected = self.prompt_bool(self.reporter.vocab['hunk'])
346
289
                if not self.reporter.invert_diff:
347
290
                    selected = (not selected)
348
291
                if selected:
351
294
                else:
352
295
                    offset -= (hunk.mod_range - hunk.orig_range)
353
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))
354
304
        if self.reporter.invert_diff:
355
 
            change_count = len(final_hunks)
356
 
        else:
357
 
            change_count = len(parsed.hunks) - len(final_hunks)
358
 
        patched = patches.iter_patched_from_hunks(target_lines,
359
 
                                                  final_hunks)
360
 
        lines = list(patched)
361
 
        return lines, change_count
362
 
 
363
 
    def _edit_file(self, file_id, work_tree_lines):
364
 
        """
365
 
        :param file_id: id of the file to edit.
366
 
        :param work_tree_lines: Line contents of the file in the working tree.
367
 
        :return: (lines, change_region_count), where lines is the new line
368
 
            content of the file, and change_region_count is the number of
369
 
            changed regions.
370
 
        """
371
 
        lines = osutils.split_lines(self.change_editor.edit_file(file_id))
372
 
        return lines, self._count_changed_regions(work_tree_lines, lines)
373
 
 
374
 
    @staticmethod
375
 
    def _count_changed_regions(old_lines, new_lines):
376
 
        matcher = patiencediff.PatienceSequenceMatcher(None, old_lines,
377
 
                                                       new_lines)
378
 
        blocks = matcher.get_matching_blocks()
379
 
        return len(blocks) - 2
 
305
            return len(final_hunks)
 
306
        return len(parsed.hunks) - len(final_hunks)
380
307
 
381
308
 
382
309
class Unshelver(object):
383
310
    """Unshelve changes into a working tree."""
384
311
 
385
312
    @classmethod
386
 
    def from_args(klass, shelf_id=None, action='apply', directory='.',
387
 
                  write_diff_to=None):
 
313
    def from_args(klass, shelf_id=None, action='apply', directory='.'):
388
314
        """Create an unshelver from commandline arguments.
389
315
 
390
 
        The returned shelver will have a tree that is locked and should
391
 
        be unlocked.
392
 
 
393
316
        :param shelf_id: Integer id of the shelf, as a string.
394
317
        :param action: action to perform.  May be 'apply', 'dry-run',
395
 
            'delete', 'preview'.
 
318
            'delete'.
396
319
        :param directory: The directory to unshelve changes into.
397
 
        :param write_diff_to: See Unshelver.__init__().
398
320
        """
399
321
        tree, path = workingtree.WorkingTree.open_containing(directory)
400
 
        tree.lock_tree_write()
401
 
        try:
402
 
            manager = tree.get_shelf_manager()
403
 
            if shelf_id is not None:
404
 
                try:
405
 
                    shelf_id = int(shelf_id)
406
 
                except ValueError:
407
 
                    raise errors.InvalidShelfId(shelf_id)
408
 
            else:
409
 
                shelf_id = manager.last_shelf()
410
 
                if shelf_id is None:
411
 
                    raise errors.BzrCommandError('No changes are shelved.')
412
 
            apply_changes = True
413
 
            delete_shelf = True
414
 
            read_shelf = True
415
 
            show_diff = False
416
 
            if action == 'dry-run':
417
 
                apply_changes = False
418
 
                delete_shelf = False
419
 
            elif action == 'preview':
420
 
                apply_changes = False
421
 
                delete_shelf = False
422
 
                show_diff = True
423
 
            elif action == 'delete-only':
424
 
                apply_changes = False
425
 
                read_shelf = False
426
 
            elif action == 'keep':
427
 
                apply_changes = True
428
 
                delete_shelf = False
429
 
        except:
430
 
            tree.unlock()
431
 
            raise
 
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
432
342
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
433
 
                     read_shelf, show_diff, write_diff_to)
 
343
                     read_shelf)
434
344
 
435
345
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
436
 
                 delete_shelf=True, read_shelf=True, show_diff=False,
437
 
                 write_diff_to=None):
 
346
                 delete_shelf=True, read_shelf=True):
438
347
        """Constructor.
439
348
 
440
349
        :param tree: The working tree to unshelve into.
444
353
            working tree.
445
354
        :param delete_shelf: If True, delete the changes from the shelf.
446
355
        :param read_shelf: If True, read the changes from the shelf.
447
 
        :param show_diff: If True, show the diff that would result from
448
 
            unshelving the changes.
449
 
        :param write_diff_to: A file-like object where the diff will be
450
 
            written to. If None, ui.ui_factory.make_output_stream() will
451
 
            be used.
452
356
        """
453
357
        self.tree = tree
454
358
        manager = tree.get_shelf_manager()
457
361
        self.apply_changes = apply_changes
458
362
        self.delete_shelf = delete_shelf
459
363
        self.read_shelf = read_shelf
460
 
        self.show_diff = show_diff
461
 
        self.write_diff_to = write_diff_to
462
364
 
463
365
    def run(self):
464
366
        """Perform the unshelving operation."""
465
 
        self.tree.lock_tree_write()
 
367
        self.tree.lock_write()
466
368
        cleanups = [self.tree.unlock]
467
369
        try:
468
370
            if self.read_shelf:
469
 
                trace.note('Using changes with id "%d".' % self.shelf_id)
470
371
                unshelver = self.manager.get_unshelver(self.shelf_id)
471
372
                cleanups.append(unshelver.finalize)
472
373
                if unshelver.message is not None:
473
374
                    trace.note('Message: %s' % unshelver.message)
474
375
                change_reporter = delta._ChangeReporter()
475
 
                merger = unshelver.make_merger(None)
476
 
                merger.change_reporter = change_reporter
477
 
                if self.apply_changes:
478
 
                    merger.do_merge()
479
 
                elif self.show_diff:
480
 
                    self.write_diff(merger)
481
 
                else:
482
 
                    self.show_changes(merger)
 
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()
483
386
            if self.delete_shelf:
484
387
                self.manager.delete_shelf(self.shelf_id)
485
 
                trace.note('Deleted changes with id "%d".' % self.shelf_id)
486
388
        finally:
487
389
            for cleanup in reversed(cleanups):
488
390
                cleanup()
489
391
 
490
 
    def write_diff(self, merger):
491
 
        """Write this operation's diff to self.write_diff_to."""
492
 
        tree_merger = merger.make_merger()
493
 
        tt = tree_merger.make_preview_transform()
494
 
        new_tree = tt.get_preview_tree()
495
 
        if self.write_diff_to is None:
496
 
            self.write_diff_to = ui.ui_factory.make_output_stream()
497
 
        path_encoding = osutils.get_diff_header_encoding()
498
 
        diff.show_diff_trees(merger.this_tree, new_tree, self.write_diff_to,
499
 
            path_encoding=path_encoding)
500
 
        tt.finalize()
501
 
 
502
392
    def show_changes(self, merger):
503
393
        """Show the changes that this operation specifies."""
504
394
        tree_merger = merger.make_merger()