/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: John Arbash Meinel
  • Date: 2009-12-02 23:09:40 UTC
  • mfrom: (4853.1.1 whitespace)
  • mto: This revision was merged to the branch mainline in revision 4856.
  • Revision ID: john@arbash-meinel.com-20091202230940-7n2aydoxngdqxzld
Strip trailing whitespace from doc files by Patrick Regan.

Resolve one small conflict with another doc edit.
Also, revert the changes to all the .pdf and .png files. We shouldn't
be touching them as they are pure-binary files.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2008 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
    commands,
 
26
    delta,
 
27
    diff,
 
28
    errors,
 
29
    osutils,
 
30
    patches,
 
31
    patiencediff,
 
32
    shelf,
 
33
    textfile,
 
34
    trace,
 
35
    ui,
 
36
    workingtree,
 
37
)
 
38
 
 
39
 
 
40
class UseEditor(Exception):
 
41
    """Use an editor instead of selecting hunks."""
 
42
 
 
43
 
 
44
class ShelfReporter(object):
 
45
 
 
46
    vocab = {'add file': 'Shelve adding file "%(path)s"?',
 
47
             'binary': 'Shelve binary changes?',
 
48
             'change kind': 'Shelve changing "%s" from %(other)s'
 
49
             ' to %(this)s?',
 
50
             'delete file': 'Shelve removing file "%(path)s"?',
 
51
             'final': 'Shelve %d change(s)?',
 
52
             'hunk': 'Shelve?',
 
53
             'modify target': 'Shelve changing target of'
 
54
             ' "%(path)s" from "%(other)s" to "%(this)s"?',
 
55
             'rename': '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('Changes shelved with id "%d".' % shelf_id)
 
71
 
 
72
    def changes_destroyed(self):
 
73
        """Report that changes were made without shelving."""
 
74
        trace.note('Selected changes destroyed.')
 
75
 
 
76
    def selected_changes(self, transform):
 
77
        """Report the changes that were selected."""
 
78
        trace.note("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': 'Delete file "%(path)s"?',
 
99
             'binary': 'Apply binary changes?',
 
100
             'change kind': 'Change "%(path)s" from %(this)s'
 
101
             ' to %(other)s?',
 
102
             'delete file': 'Add file "%(path)s"?',
 
103
             'final': 'Apply %d change(s)?',
 
104
             'hunk': 'Apply change?',
 
105
             'modify target': 'Change target of'
 
106
             ' "%(path)s" from "%(this)s" to "%(other)s"?',
 
107
             'rename': '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='.', 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
        tree, path = workingtree.WorkingTree.open_containing(directory)
 
173
        # Ensure that tree is locked for the lifetime of target_tree, as
 
174
        # target tree may be reading from the same dirstate.
 
175
        tree.lock_tree_write()
 
176
        try:
 
177
            target_tree = builtins._get_one_revision_tree('shelf2', revision,
 
178
                tree.branch, tree)
 
179
            files = builtins.safe_relpath_files(tree, file_list)
 
180
            return klass(tree, target_tree, diff_writer, all, all, files,
 
181
                         message, destroy)
 
182
        finally:
 
183
            tree.unlock()
 
184
 
 
185
    def run(self):
 
186
        """Interactively shelve the changes."""
 
187
        creator = shelf.ShelfCreator(self.work_tree, self.target_tree,
 
188
                                     self.file_list)
 
189
        self.tempdir = tempfile.mkdtemp()
 
190
        changes_shelved = 0
 
191
        try:
 
192
            for change in creator.iter_shelvable():
 
193
                if change[0] == 'modify text':
 
194
                    try:
 
195
                        changes_shelved += self.handle_modify_text(creator,
 
196
                                                                   change[1])
 
197
                    except errors.BinaryFile:
 
198
                        if self.prompt_bool(self.reporter.vocab['binary']):
 
199
                            changes_shelved += 1
 
200
                            creator.shelve_content_change(change[1])
 
201
                else:
 
202
                    if self.prompt_bool(self.reporter.prompt_change(change)):
 
203
                        creator.shelve_change(change)
 
204
                        changes_shelved += 1
 
205
            if changes_shelved > 0:
 
206
                self.reporter.selected_changes(creator.work_transform)
 
207
                if (self.auto_apply or self.prompt_bool(
 
208
                    self.reporter.vocab['final'] % changes_shelved)):
 
209
                    if self.destroy:
 
210
                        creator.transform()
 
211
                        self.reporter.changes_destroyed()
 
212
                    else:
 
213
                        shelf_id = self.manager.shelve_changes(creator,
 
214
                                                               self.message)
 
215
                        self.reporter.shelved_id(shelf_id)
 
216
            else:
 
217
                self.reporter.no_changes()
 
218
        finally:
 
219
            shutil.rmtree(self.tempdir)
 
220
            creator.finalize()
 
221
 
 
222
    def finalize(self):
 
223
        if self.change_editor is not None:
 
224
            self.change_editor.finish()
 
225
        self.work_tree.unlock()
 
226
 
 
227
 
 
228
    def get_parsed_patch(self, file_id, invert=False):
 
229
        """Return a parsed version of a file's patch.
 
230
 
 
231
        :param file_id: The id of the file to generate a patch for.
 
232
        :param invert: If True, provide an inverted patch (insertions displayed
 
233
            as removals, removals displayed as insertions).
 
234
        :return: A patches.Patch.
 
235
        """
 
236
        diff_file = StringIO()
 
237
        if invert:
 
238
            old_tree = self.work_tree
 
239
            new_tree = self.target_tree
 
240
        else:
 
241
            old_tree = self.target_tree
 
242
            new_tree = self.work_tree
 
243
        old_path = old_tree.id2path(file_id)
 
244
        new_path = new_tree.id2path(file_id)
 
245
        text_differ = diff.DiffText(old_tree, new_tree, diff_file)
 
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
        """Create an unshelver from commandline arguments.
 
388
 
 
389
        The returned shelver wil have a tree that is locked and should
 
390
        be unlocked.
 
391
 
 
392
        :param shelf_id: Integer id of the shelf, as a string.
 
393
        :param action: action to perform.  May be 'apply', 'dry-run',
 
394
            'delete'.
 
395
        :param directory: The directory to unshelve changes into.
 
396
        """
 
397
        tree, path = workingtree.WorkingTree.open_containing(directory)
 
398
        tree.lock_tree_write()
 
399
        try:
 
400
            manager = tree.get_shelf_manager()
 
401
            if shelf_id is not None:
 
402
                try:
 
403
                    shelf_id = int(shelf_id)
 
404
                except ValueError:
 
405
                    raise errors.InvalidShelfId(shelf_id)
 
406
            else:
 
407
                shelf_id = manager.last_shelf()
 
408
                if shelf_id is None:
 
409
                    raise errors.BzrCommandError('No changes are shelved.')
 
410
                trace.note('Unshelving changes with id "%d".' % shelf_id)
 
411
            apply_changes = True
 
412
            delete_shelf = True
 
413
            read_shelf = True
 
414
            if action == 'dry-run':
 
415
                apply_changes = False
 
416
                delete_shelf = False
 
417
            if action == 'delete-only':
 
418
                apply_changes = False
 
419
                read_shelf = False
 
420
        except:
 
421
            tree.unlock()
 
422
            raise
 
423
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
 
424
                     read_shelf)
 
425
 
 
426
    def __init__(self, tree, manager, shelf_id, apply_changes=True,
 
427
                 delete_shelf=True, read_shelf=True):
 
428
        """Constructor.
 
429
 
 
430
        :param tree: The working tree to unshelve into.
 
431
        :param manager: The ShelveManager containing the shelved changes.
 
432
        :param shelf_id:
 
433
        :param apply_changes: If True, apply the shelved changes to the
 
434
            working tree.
 
435
        :param delete_shelf: If True, delete the changes from the shelf.
 
436
        :param read_shelf: If True, read the changes from the shelf.
 
437
        """
 
438
        self.tree = tree
 
439
        manager = tree.get_shelf_manager()
 
440
        self.manager = manager
 
441
        self.shelf_id = shelf_id
 
442
        self.apply_changes = apply_changes
 
443
        self.delete_shelf = delete_shelf
 
444
        self.read_shelf = read_shelf
 
445
 
 
446
    def run(self):
 
447
        """Perform the unshelving operation."""
 
448
        self.tree.lock_tree_write()
 
449
        cleanups = [self.tree.unlock]
 
450
        try:
 
451
            if self.read_shelf:
 
452
                unshelver = self.manager.get_unshelver(self.shelf_id)
 
453
                cleanups.append(unshelver.finalize)
 
454
                if unshelver.message is not None:
 
455
                    trace.note('Message: %s' % unshelver.message)
 
456
                change_reporter = delta._ChangeReporter()
 
457
                task = ui.ui_factory.nested_progress_bar()
 
458
                try:
 
459
                    merger = unshelver.make_merger(task)
 
460
                    merger.change_reporter = change_reporter
 
461
                    if self.apply_changes:
 
462
                        merger.do_merge()
 
463
                    else:
 
464
                        self.show_changes(merger)
 
465
                finally:
 
466
                    task.finished()
 
467
            if self.delete_shelf:
 
468
                self.manager.delete_shelf(self.shelf_id)
 
469
        finally:
 
470
            for cleanup in reversed(cleanups):
 
471
                cleanup()
 
472
 
 
473
    def show_changes(self, merger):
 
474
        """Show the changes that this operation specifies."""
 
475
        tree_merger = merger.make_merger()
 
476
        # This implicitly shows the changes via the reporter, so we're done...
 
477
        tt = tree_merger.make_preview_transform()
 
478
        tt.finalize()