~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/shelf_ui.py

  • Committer: Brian de Alwis
  • Date: 2009-09-24 19:51:37 UTC
  • mto: (4715.4.1 integration)
  • mto: This revision was merged to the branch mainline in revision 4727.
  • Revision ID: bsd@acm.org-20090924195137-wubyeqv515mkigi8
Introduce new mailer to support MacOS X's Mail.app

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,
175
167
        try:
176
168
            target_tree = builtins._get_one_revision_tree('shelf2', revision,
177
169
                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:
 
170
            files = builtins.safe_relpath_files(tree, file_list)
 
171
        except:
182
172
            tree.unlock()
 
173
            raise
 
174
        return klass(tree, target_tree, diff_writer, all, all, files, message,
 
175
                     destroy)
183
176
 
184
177
    def run(self):
185
178
        """Interactively shelve the changes."""
218
211
            shutil.rmtree(self.tempdir)
219
212
            creator.finalize()
220
213
 
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
214
    def get_parsed_patch(self, file_id, invert=False):
228
215
        """Return a parsed version of a file's patch.
229
216
 
241
228
            new_tree = self.work_tree
242
229
        old_path = old_tree.id2path(file_id)
243
230
        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())
 
231
        text_differ = diff.DiffText(old_tree, new_tree, diff_file)
246
232
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
247
233
        diff_file.seek(0)
248
234
        return patches.parse_patch(diff_file)
253
239
        :param message: The message to prompt a user with.
254
240
        :return: A character.
255
241
        """
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
242
        sys.stdout.write(message)
263
243
        char = osutils.getchar()
264
244
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
265
245
        sys.stdout.flush()
266
246
        return char
267
247
 
268
 
    def prompt_bool(self, question, long=False, allow_editor=False):
 
248
    def prompt_bool(self, question, long=False):
269
249
        """Prompt the user with a yes/no question.
270
250
 
271
251
        This may be overridden by self.auto.  It may also *set* self.auto.  It
275
255
        """
276
256
        if self.auto:
277
257
            return True
278
 
        editor_string = ''
279
258
        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
 
259
            prompt = ' [(y)es, (N)o, (f)inish, or (q)uit]'
283
260
        else:
284
 
            if allow_editor:
285
 
                editor_string = 'e'
286
 
            prompt = ' [yN%sfq?]' % editor_string
 
261
            prompt = ' [yNfq?]'
287
262
        char = self.prompt(question + prompt)
288
263
        if char == 'y':
289
264
            return True
290
 
        elif char == 'e' and allow_editor:
291
 
            raise UseEditor
292
265
        elif char == 'f':
293
266
            self.auto = True
294
267
            return True
300
273
            return False
301
274
 
302
275
    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
276
        """Provide diff hunk selection for modified text.
321
277
 
322
278
        If self.reporter.invert_diff is True, the diff is inverted so that
324
280
 
325
281
        :param creator: a ShelfCreator
326
282
        :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
283
        :return: number of shelved hunks.
329
284
        """
330
285
        if self.reporter.invert_diff:
331
 
            target_lines = work_tree_lines
 
286
            target_lines = self.work_tree.get_file_lines(file_id)
332
287
        else:
333
288
            target_lines = self.target_tree.get_file_lines(file_id)
334
 
        textfile.check_text_lines(work_tree_lines)
 
289
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
335
290
        textfile.check_text_lines(target_lines)
336
291
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
337
292
        final_hunks = []
340
295
            self.diff_writer.write(parsed.get_header())
341
296
            for hunk in parsed.hunks:
342
297
                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))
 
298
                selected = self.prompt_bool(self.reporter.vocab['hunk'])
346
299
                if not self.reporter.invert_diff:
347
300
                    selected = (not selected)
348
301
                if selected:
351
304
                else:
352
305
                    offset -= (hunk.mod_range - hunk.orig_range)
353
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))
354
314
        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
 
315
            return len(final_hunks)
 
316
        return len(parsed.hunks) - len(final_hunks)
380
317
 
381
318
 
382
319
class Unshelver(object):
383
320
    """Unshelve changes into a working tree."""
384
321
 
385
322
    @classmethod
386
 
    def from_args(klass, shelf_id=None, action='apply', directory='.',
387
 
                  write_diff_to=None):
 
323
    def from_args(klass, shelf_id=None, action='apply', directory='.'):
388
324
        """Create an unshelver from commandline arguments.
389
325
 
390
 
        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
391
327
        be unlocked.
392
328
 
393
329
        :param shelf_id: Integer id of the shelf, as a string.
394
330
        :param action: action to perform.  May be 'apply', 'dry-run',
395
 
            'delete', 'preview'.
 
331
            'delete'.
396
332
        :param directory: The directory to unshelve changes into.
397
 
        :param write_diff_to: See Unshelver.__init__().
398
333
        """
399
334
        tree, path = workingtree.WorkingTree.open_containing(directory)
400
335
        tree.lock_tree_write()
409
344
                shelf_id = manager.last_shelf()
410
345
                if shelf_id is None:
411
346
                    raise errors.BzrCommandError('No changes are shelved.')
 
347
                trace.note('Unshelving changes with id "%d".' % shelf_id)
412
348
            apply_changes = True
413
349
            delete_shelf = True
414
350
            read_shelf = True
415
 
            show_diff = False
416
351
            if action == 'dry-run':
417
352
                apply_changes = False
418
353
                delete_shelf = False
419
 
            elif action == 'preview':
420
 
                apply_changes = False
421
 
                delete_shelf = False
422
 
                show_diff = True
423
 
            elif action == 'delete-only':
 
354
            if action == 'delete-only':
424
355
                apply_changes = False
425
356
                read_shelf = False
426
 
            elif action == 'keep':
427
 
                apply_changes = True
428
 
                delete_shelf = False
429
357
        except:
430
358
            tree.unlock()
431
359
            raise
432
360
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
433
 
                     read_shelf, show_diff, write_diff_to)
 
361
                     read_shelf)
434
362
 
435
363
    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):
 
364
                 delete_shelf=True, read_shelf=True):
438
365
        """Constructor.
439
366
 
440
367
        :param tree: The working tree to unshelve into.
444
371
            working tree.
445
372
        :param delete_shelf: If True, delete the changes from the shelf.
446
373
        :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
374
        """
453
375
        self.tree = tree
454
376
        manager = tree.get_shelf_manager()
457
379
        self.apply_changes = apply_changes
458
380
        self.delete_shelf = delete_shelf
459
381
        self.read_shelf = read_shelf
460
 
        self.show_diff = show_diff
461
 
        self.write_diff_to = write_diff_to
462
382
 
463
383
    def run(self):
464
384
        """Perform the unshelving operation."""
466
386
        cleanups = [self.tree.unlock]
467
387
        try:
468
388
            if self.read_shelf:
469
 
                trace.note('Using changes with id "%d".' % self.shelf_id)
470
389
                unshelver = self.manager.get_unshelver(self.shelf_id)
471
390
                cleanups.append(unshelver.finalize)
472
391
                if unshelver.message is not None:
473
392
                    trace.note('Message: %s' % unshelver.message)
474
393
                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)
 
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()
483
404
            if self.delete_shelf:
484
405
                self.manager.delete_shelf(self.shelf_id)
485
 
                trace.note('Deleted changes with id "%d".' % self.shelf_id)
486
406
        finally:
487
407
            for cleanup in reversed(cleanups):
488
408
                cleanup()
489
409
 
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
410
    def show_changes(self, merger):
503
411
        """Show the changes that this operation specifies."""
504
412
        tree_merger = merger.make_merger()