~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/shelf_ui.py

  • Committer: Martin
  • Date: 2009-11-28 00:48:03 UTC
  • mto: This revision was merged to the branch mainline in revision 4853.
  • Revision ID: gzlist@googlemail.com-20091128004803-nirz54wazyj0waf8
MergeDirective.from_lines claims to want an iterable but currently requires a list, rewrite so it really wants an iterable

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