/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: Vincent Ladeuil
  • Date: 2012-01-18 14:09:19 UTC
  • mto: This revision was merged to the branch mainline in revision 6468.
  • Revision ID: v.ladeuil+lp@free.fr-20120118140919-rlvdrhpc0nq1lbwi
Change set/remove to require a lock for the branch config files.

This means that tests (or any plugin for that matter) do not requires an
explicit lock on the branch anymore to change a single option. This also
means the optimisation becomes "opt-in" and as such won't be as
spectacular as it may be and/or harder to get right (nothing fails
anymore).

This reduces the diff by ~300 lines.

Code/tests that were updating more than one config option is still taking
a lock to at least avoid some IOs and demonstrate the benefits through
the decreased number of hpss calls.

The duplication between BranchStack and BranchOnlyStack will be removed
once the same sharing is in place for local config files, at which point
the Stack class itself may be able to host the changes.

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
from __future__ import absolute_import
 
18
 
 
19
from cStringIO import StringIO
 
20
import shutil
 
21
import sys
 
22
import tempfile
 
23
 
 
24
from bzrlib import (
 
25
    builtins,
 
26
    delta,
 
27
    diff,
 
28
    errors,
 
29
    osutils,
 
30
    patches,
 
31
    patiencediff,
 
32
    shelf,
 
33
    textfile,
 
34
    trace,
 
35
    ui,
 
36
    workingtree,
 
37
)
 
38
from bzrlib.i18n import gettext
 
39
 
 
40
class UseEditor(Exception):
 
41
    """Use an editor instead of selecting hunks."""
 
42
 
 
43
 
 
44
class ShelfReporter(object):
 
45
 
 
46
    vocab = {'add file': gettext('Shelve adding file "%(path)s"?'),
 
47
             'binary': gettext('Shelve binary changes?'),
 
48
             'change kind': gettext('Shelve changing "%s" from %(other)s'
 
49
             ' to %(this)s?'),
 
50
             'delete file': gettext('Shelve removing file "%(path)s"?'),
 
51
             'final': gettext('Shelve %d change(s)?'),
 
52
             'hunk': gettext('Shelve?'),
 
53
             'modify target': gettext('Shelve changing target of'
 
54
             ' "%(path)s" from "%(other)s" to "%(this)s"?'),
 
55
             'rename': gettext('Shelve renaming "%(other)s" =>'
 
56
                        ' "%(this)s"?')
 
57
             }
 
58
 
 
59
    invert_diff = False
 
60
 
 
61
    def __init__(self):
 
62
        self.delta_reporter = delta._ChangeReporter()
 
63
 
 
64
    def no_changes(self):
 
65
        """Report that no changes were selected to apply."""
 
66
        trace.warning('No changes to shelve.')
 
67
 
 
68
    def shelved_id(self, shelf_id):
 
69
        """Report the id changes were shelved to."""
 
70
        trace.note(gettext('Changes shelved with id "%d".') % shelf_id)
 
71
 
 
72
    def changes_destroyed(self):
 
73
        """Report that changes were made without shelving."""
 
74
        trace.note(gettext('Selected changes destroyed.'))
 
75
 
 
76
    def selected_changes(self, transform):
 
77
        """Report the changes that were selected."""
 
78
        trace.note(gettext("Selected changes:"))
 
79
        changes = transform.iter_changes()
 
80
        delta.report_changes(changes, self.delta_reporter)
 
81
 
 
82
    def prompt_change(self, change):
 
83
        """Determine the prompt for a change to apply."""
 
84
        if change[0] == 'rename':
 
85
            vals = {'this': change[3], 'other': change[2]}
 
86
        elif change[0] == 'change kind':
 
87
            vals = {'path': change[4], 'other': change[2], 'this': change[3]}
 
88
        elif change[0] == 'modify target':
 
89
            vals = {'path': change[2], 'other': change[3], 'this': change[4]}
 
90
        else:
 
91
            vals = {'path': change[3]}
 
92
        prompt = self.vocab[change[0]] % vals
 
93
        return prompt
 
94
 
 
95
 
 
96
class ApplyReporter(ShelfReporter):
 
97
 
 
98
    vocab = {'add file': gettext('Delete file "%(path)s"?'),
 
99
             'binary': gettext('Apply binary changes?'),
 
100
             'change kind': gettext('Change "%(path)s" from %(this)s'
 
101
             ' to %(other)s?'),
 
102
             'delete file': gettext('Add file "%(path)s"?'),
 
103
             'final': gettext('Apply %d change(s)?'),
 
104
             'hunk': gettext('Apply change?'),
 
105
             'modify target': gettext('Change target of'
 
106
             ' "%(path)s" from "%(this)s" to "%(other)s"?'),
 
107
             'rename': gettext('Rename "%(this)s" => "%(other)s"?'),
 
108
             }
 
109
 
 
110
    invert_diff = True
 
111
 
 
112
    def changes_destroyed(self):
 
113
        pass
 
114
 
 
115
 
 
116
class Shelver(object):
 
117
    """Interactively shelve the changes in a working tree."""
 
118
 
 
119
    def __init__(self, work_tree, target_tree, diff_writer=None, auto=False,
 
120
                 auto_apply=False, file_list=None, message=None,
 
121
                 destroy=False, manager=None, reporter=None):
 
122
        """Constructor.
 
123
 
 
124
        :param work_tree: The working tree to shelve changes from.
 
125
        :param target_tree: The "unchanged" / old tree to compare the
 
126
            work_tree to.
 
127
        :param auto: If True, shelve each possible change.
 
128
        :param auto_apply: If True, shelve changes with no final prompt.
 
129
        :param file_list: If supplied, only files in this list may be shelved.
 
130
        :param message: The message to associate with the shelved changes.
 
131
        :param destroy: Change the working tree without storing the shelved
 
132
            changes.
 
133
        :param manager: The shelf manager to use.
 
134
        :param reporter: Object for reporting changes to user.
 
135
        """
 
136
        self.work_tree = work_tree
 
137
        self.target_tree = target_tree
 
138
        self.diff_writer = diff_writer
 
139
        if self.diff_writer is None:
 
140
            self.diff_writer = sys.stdout
 
141
        if manager is None:
 
142
            manager = work_tree.get_shelf_manager()
 
143
        self.manager = manager
 
144
        self.auto = auto
 
145
        self.auto_apply = auto_apply
 
146
        self.file_list = file_list
 
147
        self.message = message
 
148
        self.destroy = destroy
 
149
        if reporter is None:
 
150
            reporter = ShelfReporter()
 
151
        self.reporter = reporter
 
152
        config = self.work_tree.branch.get_config()
 
153
        self.change_editor = config.get_change_editor(target_tree, work_tree)
 
154
        self.work_tree.lock_tree_write()
 
155
 
 
156
    @classmethod
 
157
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
 
158
                  message=None, directory=None, destroy=False):
 
159
        """Create a shelver from commandline arguments.
 
160
 
 
161
        The returned shelver wil have a work_tree that is locked and should
 
162
        be unlocked.
 
163
 
 
164
        :param revision: RevisionSpec of the revision to compare to.
 
165
        :param all: If True, shelve all changes without prompting.
 
166
        :param file_list: If supplied, only files in this list may be  shelved.
 
167
        :param message: The message to associate with the shelved changes.
 
168
        :param directory: The directory containing the working tree.
 
169
        :param destroy: Change the working tree without storing the shelved
 
170
            changes.
 
171
        """
 
172
        if directory is None:
 
173
            directory = u'.'
 
174
        elif file_list:
 
175
            file_list = [osutils.pathjoin(directory, f) for f in file_list]
 
176
        tree, path = workingtree.WorkingTree.open_containing(directory)
 
177
        # Ensure that tree is locked for the lifetime of target_tree, as
 
178
        # target tree may be reading from the same dirstate.
 
179
        tree.lock_tree_write()
 
180
        try:
 
181
            target_tree = builtins._get_one_revision_tree('shelf2', revision,
 
182
                tree.branch, tree)
 
183
            files = tree.safe_relpath_files(file_list)
 
184
            return klass(tree, target_tree, diff_writer, all, all, files,
 
185
                         message, destroy)
 
186
        finally:
 
187
            tree.unlock()
 
188
 
 
189
    def run(self):
 
190
        """Interactively shelve the changes."""
 
191
        creator = shelf.ShelfCreator(self.work_tree, self.target_tree,
 
192
                                     self.file_list)
 
193
        self.tempdir = tempfile.mkdtemp()
 
194
        changes_shelved = 0
 
195
        try:
 
196
            for change in creator.iter_shelvable():
 
197
                if change[0] == 'modify text':
 
198
                    try:
 
199
                        changes_shelved += self.handle_modify_text(creator,
 
200
                                                                   change[1])
 
201
                    except errors.BinaryFile:
 
202
                        if self.prompt_bool(self.reporter.vocab['binary']):
 
203
                            changes_shelved += 1
 
204
                            creator.shelve_content_change(change[1])
 
205
                else:
 
206
                    if self.prompt_bool(self.reporter.prompt_change(change)):
 
207
                        creator.shelve_change(change)
 
208
                        changes_shelved += 1
 
209
            if changes_shelved > 0:
 
210
                self.reporter.selected_changes(creator.work_transform)
 
211
                if (self.auto_apply or self.prompt_bool(
 
212
                    self.reporter.vocab['final'] % changes_shelved)):
 
213
                    if self.destroy:
 
214
                        creator.transform()
 
215
                        self.reporter.changes_destroyed()
 
216
                    else:
 
217
                        shelf_id = self.manager.shelve_changes(creator,
 
218
                                                               self.message)
 
219
                        self.reporter.shelved_id(shelf_id)
 
220
            else:
 
221
                self.reporter.no_changes()
 
222
        finally:
 
223
            shutil.rmtree(self.tempdir)
 
224
            creator.finalize()
 
225
 
 
226
    def finalize(self):
 
227
        if self.change_editor is not None:
 
228
            self.change_editor.finish()
 
229
        self.work_tree.unlock()
 
230
 
 
231
 
 
232
    def get_parsed_patch(self, file_id, invert=False):
 
233
        """Return a parsed version of a file's patch.
 
234
 
 
235
        :param file_id: The id of the file to generate a patch for.
 
236
        :param invert: If True, provide an inverted patch (insertions displayed
 
237
            as removals, removals displayed as insertions).
 
238
        :return: A patches.Patch.
 
239
        """
 
240
        diff_file = StringIO()
 
241
        if invert:
 
242
            old_tree = self.work_tree
 
243
            new_tree = self.target_tree
 
244
        else:
 
245
            old_tree = self.target_tree
 
246
            new_tree = self.work_tree
 
247
        old_path = old_tree.id2path(file_id)
 
248
        new_path = new_tree.id2path(file_id)
 
249
        text_differ = diff.DiffText(old_tree, new_tree, diff_file,
 
250
            path_encoding=osutils.get_terminal_encoding())
 
251
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
 
252
        diff_file.seek(0)
 
253
        return patches.parse_patch(diff_file)
 
254
 
 
255
    def prompt(self, message, choices, default):
 
256
        return ui.ui_factory.choose(message, choices, default=default)
 
257
 
 
258
    def prompt_bool(self, question, allow_editor=False):
 
259
        """Prompt the user with a yes/no question.
 
260
 
 
261
        This may be overridden by self.auto.  It may also *set* self.auto.  It
 
262
        may also raise UserAbort.
 
263
        :param question: The question to ask the user.
 
264
        :return: True or False
 
265
        """
 
266
        if self.auto:
 
267
            return True
 
268
        alternatives_chars = 'yn'
 
269
        alternatives = '&yes\n&No'
 
270
        if allow_editor:
 
271
            alternatives_chars += 'e'
 
272
            alternatives += '\n&edit manually'
 
273
        alternatives_chars += 'fq'
 
274
        alternatives += '\n&finish\n&quit'
 
275
        choice = self.prompt(question, alternatives, 1)
 
276
        if choice is None:
 
277
            # EOF.
 
278
            char = 'n'
 
279
        else:
 
280
            char = alternatives_chars[choice]
 
281
        if char == 'y':
 
282
            return True
 
283
        elif char == 'e' and allow_editor:
 
284
            raise UseEditor
 
285
        elif char == 'f':
 
286
            self.auto = True
 
287
            return True
 
288
        if char == 'q':
 
289
            raise errors.UserAbort()
 
290
        else:
 
291
            return False
 
292
 
 
293
    def handle_modify_text(self, creator, file_id):
 
294
        """Handle modified text, by using hunk selection or file editing.
 
295
 
 
296
        :param creator: A ShelfCreator.
 
297
        :param file_id: The id of the file that was modified.
 
298
        :return: The number of changes.
 
299
        """
 
300
        work_tree_lines = self.work_tree.get_file_lines(file_id)
 
301
        try:
 
302
            lines, change_count = self._select_hunks(creator, file_id,
 
303
                                                     work_tree_lines)
 
304
        except UseEditor:
 
305
            lines, change_count = self._edit_file(file_id, work_tree_lines)
 
306
        if change_count != 0:
 
307
            creator.shelve_lines(file_id, lines)
 
308
        return change_count
 
309
 
 
310
    def _select_hunks(self, creator, file_id, work_tree_lines):
 
311
        """Provide diff hunk selection for modified text.
 
312
 
 
313
        If self.reporter.invert_diff is True, the diff is inverted so that
 
314
        insertions are displayed as removals and vice versa.
 
315
 
 
316
        :param creator: a ShelfCreator
 
317
        :param file_id: The id of the file to shelve.
 
318
        :param work_tree_lines: Line contents of the file in the working tree.
 
319
        :return: number of shelved hunks.
 
320
        """
 
321
        if self.reporter.invert_diff:
 
322
            target_lines = work_tree_lines
 
323
        else:
 
324
            target_lines = self.target_tree.get_file_lines(file_id)
 
325
        textfile.check_text_lines(work_tree_lines)
 
326
        textfile.check_text_lines(target_lines)
 
327
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
 
328
        final_hunks = []
 
329
        if not self.auto:
 
330
            offset = 0
 
331
            self.diff_writer.write(parsed.get_header())
 
332
            for hunk in parsed.hunks:
 
333
                self.diff_writer.write(str(hunk))
 
334
                selected = self.prompt_bool(self.reporter.vocab['hunk'],
 
335
                                            allow_editor=(self.change_editor
 
336
                                                          is not None))
 
337
                if not self.reporter.invert_diff:
 
338
                    selected = (not selected)
 
339
                if selected:
 
340
                    hunk.mod_pos += offset
 
341
                    final_hunks.append(hunk)
 
342
                else:
 
343
                    offset -= (hunk.mod_range - hunk.orig_range)
 
344
        sys.stdout.flush()
 
345
        if self.reporter.invert_diff:
 
346
            change_count = len(final_hunks)
 
347
        else:
 
348
            change_count = len(parsed.hunks) - len(final_hunks)
 
349
        patched = patches.iter_patched_from_hunks(target_lines,
 
350
                                                  final_hunks)
 
351
        lines = list(patched)
 
352
        return lines, change_count
 
353
 
 
354
    def _edit_file(self, file_id, work_tree_lines):
 
355
        """
 
356
        :param file_id: id of the file to edit.
 
357
        :param work_tree_lines: Line contents of the file in the working tree.
 
358
        :return: (lines, change_region_count), where lines is the new line
 
359
            content of the file, and change_region_count is the number of
 
360
            changed regions.
 
361
        """
 
362
        lines = osutils.split_lines(self.change_editor.edit_file(file_id))
 
363
        return lines, self._count_changed_regions(work_tree_lines, lines)
 
364
 
 
365
    @staticmethod
 
366
    def _count_changed_regions(old_lines, new_lines):
 
367
        matcher = patiencediff.PatienceSequenceMatcher(None, old_lines,
 
368
                                                       new_lines)
 
369
        blocks = matcher.get_matching_blocks()
 
370
        return len(blocks) - 2
 
371
 
 
372
 
 
373
class Unshelver(object):
 
374
    """Unshelve changes into a working tree."""
 
375
 
 
376
    @classmethod
 
377
    def from_args(klass, shelf_id=None, action='apply', directory='.',
 
378
                  write_diff_to=None):
 
379
        """Create an unshelver from commandline arguments.
 
380
 
 
381
        The returned shelver will have a tree that is locked and should
 
382
        be unlocked.
 
383
 
 
384
        :param shelf_id: Integer id of the shelf, as a string.
 
385
        :param action: action to perform.  May be 'apply', 'dry-run',
 
386
            'delete', 'preview'.
 
387
        :param directory: The directory to unshelve changes into.
 
388
        :param write_diff_to: See Unshelver.__init__().
 
389
        """
 
390
        tree, path = workingtree.WorkingTree.open_containing(directory)
 
391
        tree.lock_tree_write()
 
392
        try:
 
393
            manager = tree.get_shelf_manager()
 
394
            if shelf_id is not None:
 
395
                try:
 
396
                    shelf_id = int(shelf_id)
 
397
                except ValueError:
 
398
                    raise errors.InvalidShelfId(shelf_id)
 
399
            else:
 
400
                shelf_id = manager.last_shelf()
 
401
                if shelf_id is None:
 
402
                    raise errors.BzrCommandError(gettext('No changes are shelved.'))
 
403
            apply_changes = True
 
404
            delete_shelf = True
 
405
            read_shelf = True
 
406
            show_diff = False
 
407
            if action == 'dry-run':
 
408
                apply_changes = False
 
409
                delete_shelf = False
 
410
            elif action == 'preview':
 
411
                apply_changes = False
 
412
                delete_shelf = False
 
413
                show_diff = True
 
414
            elif action == 'delete-only':
 
415
                apply_changes = False
 
416
                read_shelf = False
 
417
            elif action == 'keep':
 
418
                apply_changes = True
 
419
                delete_shelf = False
 
420
        except:
 
421
            tree.unlock()
 
422
            raise
 
423
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
 
424
                     read_shelf, show_diff, write_diff_to)
 
425
 
 
426
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
 
427
                 delete_shelf=True, read_shelf=True, show_diff=False,
 
428
                 write_diff_to=None):
 
429
        """Constructor.
 
430
 
 
431
        :param tree: The working tree to unshelve into.
 
432
        :param manager: The ShelveManager containing the shelved changes.
 
433
        :param shelf_id:
 
434
        :param apply_changes: If True, apply the shelved changes to the
 
435
            working tree.
 
436
        :param delete_shelf: If True, delete the changes from the shelf.
 
437
        :param read_shelf: If True, read the changes from the shelf.
 
438
        :param show_diff: If True, show the diff that would result from
 
439
            unshelving the changes.
 
440
        :param write_diff_to: A file-like object where the diff will be
 
441
            written to. If None, ui.ui_factory.make_output_stream() will
 
442
            be used.
 
443
        """
 
444
        self.tree = tree
 
445
        manager = tree.get_shelf_manager()
 
446
        self.manager = manager
 
447
        self.shelf_id = shelf_id
 
448
        self.apply_changes = apply_changes
 
449
        self.delete_shelf = delete_shelf
 
450
        self.read_shelf = read_shelf
 
451
        self.show_diff = show_diff
 
452
        self.write_diff_to = write_diff_to
 
453
 
 
454
    def run(self):
 
455
        """Perform the unshelving operation."""
 
456
        self.tree.lock_tree_write()
 
457
        cleanups = [self.tree.unlock]
 
458
        try:
 
459
            if self.read_shelf:
 
460
                trace.note(gettext('Using changes with id "%d".') % self.shelf_id)
 
461
                unshelver = self.manager.get_unshelver(self.shelf_id)
 
462
                cleanups.append(unshelver.finalize)
 
463
                if unshelver.message is not None:
 
464
                    trace.note(gettext('Message: %s') % unshelver.message)
 
465
                change_reporter = delta._ChangeReporter()
 
466
                merger = unshelver.make_merger(None)
 
467
                merger.change_reporter = change_reporter
 
468
                if self.apply_changes:
 
469
                    merger.do_merge()
 
470
                elif self.show_diff:
 
471
                    self.write_diff(merger)
 
472
                else:
 
473
                    self.show_changes(merger)
 
474
            if self.delete_shelf:
 
475
                self.manager.delete_shelf(self.shelf_id)
 
476
                trace.note(gettext('Deleted changes with id "%d".') % self.shelf_id)
 
477
        finally:
 
478
            for cleanup in reversed(cleanups):
 
479
                cleanup()
 
480
 
 
481
    def write_diff(self, merger):
 
482
        """Write this operation's diff to self.write_diff_to."""
 
483
        tree_merger = merger.make_merger()
 
484
        tt = tree_merger.make_preview_transform()
 
485
        new_tree = tt.get_preview_tree()
 
486
        if self.write_diff_to is None:
 
487
            self.write_diff_to = ui.ui_factory.make_output_stream(encoding_type='exact')
 
488
        path_encoding = osutils.get_diff_header_encoding()
 
489
        diff.show_diff_trees(merger.this_tree, new_tree, self.write_diff_to,
 
490
            path_encoding=path_encoding)
 
491
        tt.finalize()
 
492
 
 
493
    def show_changes(self, merger):
 
494
        """Show the changes that this operation specifies."""
 
495
        tree_merger = merger.make_merger()
 
496
        # This implicitly shows the changes via the reporter, so we're done...
 
497
        tt = tree_merger.make_preview_transform()
 
498
        tt.finalize()