/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-07-04 06:22:11 UTC
  • mto: This revision was merged to the branch mainline in revision 5332.
  • Revision ID: robertc@robertcollins.net-20100704062211-tk9hw6bnsn5x47fm
``bzrlib.lsprof.profile`` will no longer silently generate bad threaded
profiles when concurrent profile requests are made. Instead the profile
requests will be serialised. Reentrant requests will now deadlock.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2008, 2009, 2010 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
 
 
18
from cStringIO import StringIO
 
19
import shutil
 
20
import sys
 
21
import tempfile
 
22
 
 
23
from bzrlib import (
 
24
    builtins,
 
25
    delta,
 
26
    diff,
 
27
    errors,
 
28
    osutils,
 
29
    patches,
 
30
    patiencediff,
 
31
    shelf,
 
32
    textfile,
 
33
    trace,
 
34
    ui,
 
35
    workingtree,
 
36
)
 
37
 
 
38
 
 
39
class UseEditor(Exception):
 
40
    """Use an editor instead of selecting hunks."""
 
41
 
 
42
 
 
43
class ShelfReporter(object):
 
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
 
 
60
    def __init__(self):
 
61
        self.delta_reporter = delta._ChangeReporter()
 
62
 
 
63
    def no_changes(self):
 
64
        """Report that no changes were selected to apply."""
 
65
        trace.warning('No changes to shelve.')
 
66
 
 
67
    def shelved_id(self, shelf_id):
 
68
        """Report the id changes were shelved to."""
 
69
        trace.note('Changes shelved with id "%d".' % shelf_id)
 
70
 
 
71
    def changes_destroyed(self):
 
72
        """Report that changes were made without shelving."""
 
73
        trace.note('Selected changes destroyed.')
 
74
 
 
75
    def selected_changes(self, transform):
 
76
        """Report the changes that were selected."""
 
77
        trace.note("Selected changes:")
 
78
        changes = transform.iter_changes()
 
79
        delta.report_changes(changes, self.delta_reporter)
 
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
 
 
114
 
 
115
class Shelver(object):
 
116
    """Interactively shelve the changes in a working tree."""
 
117
 
 
118
    def __init__(self, work_tree, target_tree, diff_writer=None, auto=False,
 
119
                 auto_apply=False, file_list=None, message=None,
 
120
                 destroy=False, manager=None, reporter=None):
 
121
        """Constructor.
 
122
 
 
123
        :param work_tree: The working tree to shelve changes from.
 
124
        :param target_tree: The "unchanged" / old tree to compare the
 
125
            work_tree to.
 
126
        :param auto: If True, shelve each possible change.
 
127
        :param auto_apply: If True, shelve changes with no final prompt.
 
128
        :param file_list: If supplied, only files in this list may be shelved.
 
129
        :param message: The message to associate with the shelved changes.
 
130
        :param destroy: Change the working tree without storing the shelved
 
131
            changes.
 
132
        :param manager: The shelf manager to use.
 
133
        :param reporter: Object for reporting changes to user.
 
134
        """
 
135
        self.work_tree = work_tree
 
136
        self.target_tree = target_tree
 
137
        self.diff_writer = diff_writer
 
138
        if self.diff_writer is None:
 
139
            self.diff_writer = sys.stdout
 
140
        if manager is None:
 
141
            manager = work_tree.get_shelf_manager()
 
142
        self.manager = manager
 
143
        self.auto = auto
 
144
        self.auto_apply = auto_apply
 
145
        self.file_list = file_list
 
146
        self.message = message
 
147
        self.destroy = destroy
 
148
        if reporter is None:
 
149
            reporter = ShelfReporter()
 
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()
 
154
 
 
155
    @classmethod
 
156
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
 
157
                  message=None, directory='.', destroy=False):
 
158
        """Create a shelver from commandline arguments.
 
159
 
 
160
        The returned shelver wil have a work_tree that is locked and should
 
161
        be unlocked.
 
162
 
 
163
        :param revision: RevisionSpec of the revision to compare to.
 
164
        :param all: If True, shelve all changes without prompting.
 
165
        :param file_list: If supplied, only files in this list may be  shelved.
 
166
        :param message: The message to associate with the shelved changes.
 
167
        :param directory: The directory containing the working tree.
 
168
        :param destroy: Change the working tree without storing the shelved
 
169
            changes.
 
170
        """
 
171
        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 = 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()
 
183
 
 
184
    def run(self):
 
185
        """Interactively shelve the changes."""
 
186
        creator = shelf.ShelfCreator(self.work_tree, self.target_tree,
 
187
                                     self.file_list)
 
188
        self.tempdir = tempfile.mkdtemp()
 
189
        changes_shelved = 0
 
190
        try:
 
191
            for change in creator.iter_shelvable():
 
192
                if change[0] == 'modify text':
 
193
                    try:
 
194
                        changes_shelved += self.handle_modify_text(creator,
 
195
                                                                   change[1])
 
196
                    except errors.BinaryFile:
 
197
                        if self.prompt_bool(self.reporter.vocab['binary']):
 
198
                            changes_shelved += 1
 
199
                            creator.shelve_content_change(change[1])
 
200
                else:
 
201
                    if self.prompt_bool(self.reporter.prompt_change(change)):
 
202
                        creator.shelve_change(change)
 
203
                        changes_shelved += 1
 
204
            if changes_shelved > 0:
 
205
                self.reporter.selected_changes(creator.work_transform)
 
206
                if (self.auto_apply or self.prompt_bool(
 
207
                    self.reporter.vocab['final'] % changes_shelved)):
 
208
                    if self.destroy:
 
209
                        creator.transform()
 
210
                        self.reporter.changes_destroyed()
 
211
                    else:
 
212
                        shelf_id = self.manager.shelve_changes(creator,
 
213
                                                               self.message)
 
214
                        self.reporter.shelved_id(shelf_id)
 
215
            else:
 
216
                self.reporter.no_changes()
 
217
        finally:
 
218
            shutil.rmtree(self.tempdir)
 
219
            creator.finalize()
 
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
 
 
227
    def get_parsed_patch(self, file_id, invert=False):
 
228
        """Return a parsed version of a file's patch.
 
229
 
 
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).
 
233
        :return: A patches.Patch.
 
234
        """
 
235
        diff_file = StringIO()
 
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,
 
245
            path_encoding=osutils.get_terminal_encoding())
 
246
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
 
247
        diff_file.seek(0)
 
248
        return patches.parse_patch(diff_file)
 
249
 
 
250
    def prompt(self, message):
 
251
        """Prompt the user for a character.
 
252
 
 
253
        :param message: The message to prompt a user with.
 
254
        :return: A character.
 
255
        """
 
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
        sys.stdout.write(message)
 
263
        char = osutils.getchar()
 
264
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
 
265
        sys.stdout.flush()
 
266
        return char
 
267
 
 
268
    def prompt_bool(self, question, long=False, allow_editor=False):
 
269
        """Prompt the user with a yes/no question.
 
270
 
 
271
        This may be overridden by self.auto.  It may also *set* self.auto.  It
 
272
        may also raise UserAbort.
 
273
        :param question: The question to ask the user.
 
274
        :return: True or False
 
275
        """
 
276
        if self.auto:
 
277
            return True
 
278
        editor_string = ''
 
279
        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
 
283
        else:
 
284
            if allow_editor:
 
285
                editor_string = 'e'
 
286
            prompt = ' [yN%sfq?]' % editor_string
 
287
        char = self.prompt(question + prompt)
 
288
        if char == 'y':
 
289
            return True
 
290
        elif char == 'e' and allow_editor:
 
291
            raise UseEditor
 
292
        elif char == 'f':
 
293
            self.auto = True
 
294
            return True
 
295
        elif char == '?':
 
296
            return self.prompt_bool(question, long=True)
 
297
        if char == 'q':
 
298
            raise errors.UserAbort()
 
299
        else:
 
300
            return False
 
301
 
 
302
    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
        """Provide diff hunk selection for modified text.
 
321
 
 
322
        If self.reporter.invert_diff is True, the diff is inverted so that
 
323
        insertions are displayed as removals and vice versa.
 
324
 
 
325
        :param creator: a ShelfCreator
 
326
        :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
        :return: number of shelved hunks.
 
329
        """
 
330
        if self.reporter.invert_diff:
 
331
            target_lines = work_tree_lines
 
332
        else:
 
333
            target_lines = self.target_tree.get_file_lines(file_id)
 
334
        textfile.check_text_lines(work_tree_lines)
 
335
        textfile.check_text_lines(target_lines)
 
336
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
 
337
        final_hunks = []
 
338
        if not self.auto:
 
339
            offset = 0
 
340
            self.diff_writer.write(parsed.get_header())
 
341
            for hunk in parsed.hunks:
 
342
                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))
 
346
                if not self.reporter.invert_diff:
 
347
                    selected = (not selected)
 
348
                if selected:
 
349
                    hunk.mod_pos += offset
 
350
                    final_hunks.append(hunk)
 
351
                else:
 
352
                    offset -= (hunk.mod_range - hunk.orig_range)
 
353
        sys.stdout.flush()
 
354
        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
 
380
 
 
381
 
 
382
class Unshelver(object):
 
383
    """Unshelve changes into a working tree."""
 
384
 
 
385
    @classmethod
 
386
    def from_args(klass, shelf_id=None, action='apply', directory='.',
 
387
                  write_diff_to=None):
 
388
        """Create an unshelver from commandline arguments.
 
389
 
 
390
        The returned shelver will have a tree that is locked and should
 
391
        be unlocked.
 
392
 
 
393
        :param shelf_id: Integer id of the shelf, as a string.
 
394
        :param action: action to perform.  May be 'apply', 'dry-run',
 
395
            'delete', 'preview'.
 
396
        :param directory: The directory to unshelve changes into.
 
397
        :param write_diff_to: See Unshelver.__init__().
 
398
        """
 
399
        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
 
432
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
 
433
                     read_shelf, show_diff, write_diff_to)
 
434
 
 
435
    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):
 
438
        """Constructor.
 
439
 
 
440
        :param tree: The working tree to unshelve into.
 
441
        :param manager: The ShelveManager containing the shelved changes.
 
442
        :param shelf_id:
 
443
        :param apply_changes: If True, apply the shelved changes to the
 
444
            working tree.
 
445
        :param delete_shelf: If True, delete the changes from the shelf.
 
446
        :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
        """
 
453
        self.tree = tree
 
454
        manager = tree.get_shelf_manager()
 
455
        self.manager = manager
 
456
        self.shelf_id = shelf_id
 
457
        self.apply_changes = apply_changes
 
458
        self.delete_shelf = delete_shelf
 
459
        self.read_shelf = read_shelf
 
460
        self.show_diff = show_diff
 
461
        self.write_diff_to = write_diff_to
 
462
 
 
463
    def run(self):
 
464
        """Perform the unshelving operation."""
 
465
        self.tree.lock_tree_write()
 
466
        cleanups = [self.tree.unlock]
 
467
        try:
 
468
            if self.read_shelf:
 
469
                trace.note('Using changes with id "%d".' % self.shelf_id)
 
470
                unshelver = self.manager.get_unshelver(self.shelf_id)
 
471
                cleanups.append(unshelver.finalize)
 
472
                if unshelver.message is not None:
 
473
                    trace.note('Message: %s' % unshelver.message)
 
474
                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)
 
483
            if self.delete_shelf:
 
484
                self.manager.delete_shelf(self.shelf_id)
 
485
                trace.note('Deleted changes with id "%d".' % self.shelf_id)
 
486
        finally:
 
487
            for cleanup in reversed(cleanups):
 
488
                cleanup()
 
489
 
 
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
    def show_changes(self, merger):
 
503
        """Show the changes that this operation specifies."""
 
504
        tree_merger = merger.make_merger()
 
505
        # This implicitly shows the changes via the reporter, so we're done...
 
506
        tt = tree_merger.make_preview_transform()
 
507
        tt.finalize()