~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/shelf_ui.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2009-03-13 01:01:35 UTC
  • mfrom: (4137.1.1 jamesw-integration)
  • Revision ID: pqm@pqm.ubuntu.com-20090313010135-3huetgz68wxrhsb3
(kikuyo) Small improvements to the GNU ChangeLog formatter.

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
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
18
from cStringIO import StringIO
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
 
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
38
class Shelver(object):
116
39
    """Interactively shelve the changes in a working tree."""
117
40
 
118
41
    def __init__(self, work_tree, target_tree, diff_writer=None, auto=False,
119
42
                 auto_apply=False, file_list=None, message=None,
120
 
                 destroy=False, manager=None, reporter=None):
 
43
                 destroy=False):
121
44
        """Constructor.
122
45
 
123
46
        :param work_tree: The working tree to shelve changes from.
129
52
        :param message: The message to associate with the shelved changes.
130
53
        :param destroy: Change the working tree without storing the shelved
131
54
            changes.
132
 
        :param manager: The shelf manager to use.
133
 
        :param reporter: Object for reporting changes to user.
134
55
        """
135
56
        self.work_tree = work_tree
136
57
        self.target_tree = target_tree
137
58
        self.diff_writer = diff_writer
138
59
        if self.diff_writer is None:
139
60
            self.diff_writer = sys.stdout
140
 
        if manager is None:
141
 
            manager = work_tree.get_shelf_manager()
142
 
        self.manager = manager
 
61
        self.manager = work_tree.get_shelf_manager()
143
62
        self.auto = auto
144
63
        self.auto_apply = auto_apply
145
64
        self.file_list = file_list
146
65
        self.message = message
147
66
        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
67
 
155
68
    @classmethod
156
69
    def from_args(klass, diff_writer, revision=None, all=False, file_list=None,
157
70
                  message=None, directory='.', destroy=False):
158
71
        """Create a shelver from commandline arguments.
159
72
 
160
 
        The returned shelver wil have a work_tree that is locked and should
161
 
        be unlocked.
162
 
 
163
73
        :param revision: RevisionSpec of the revision to compare to.
164
74
        :param all: If True, shelve all changes without prompting.
165
75
        :param file_list: If supplied, only files in this list may be  shelved.
169
79
            changes.
170
80
        """
171
81
        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 = tree.safe_relpath_files(file_list)
179
 
            return klass(tree, target_tree, diff_writer, all, all, files,
180
 
                         message, destroy)
181
 
        finally:
182
 
            tree.unlock()
 
82
        target_tree = builtins._get_one_revision_tree('shelf2', revision,
 
83
            tree.branch, tree)
 
84
        files = builtins.safe_relpath_files(tree, file_list)
 
85
        return klass(tree, target_tree, diff_writer, all, all, files, message,
 
86
                     destroy)
183
87
 
184
88
    def run(self):
185
89
        """Interactively shelve the changes."""
194
98
                        changes_shelved += self.handle_modify_text(creator,
195
99
                                                                   change[1])
196
100
                    except errors.BinaryFile:
197
 
                        if self.prompt_bool(self.reporter.vocab['binary']):
 
101
                        if self.prompt_bool('Shelve binary changes?'):
198
102
                            changes_shelved += 1
199
103
                            creator.shelve_content_change(change[1])
200
 
                else:
201
 
                    if self.prompt_bool(self.reporter.prompt_change(change)):
202
 
                        creator.shelve_change(change)
 
104
                if change[0] == 'add file':
 
105
                    if self.prompt_bool('Shelve adding file "%s"?'
 
106
                                        % change[3]):
 
107
                        creator.shelve_creation(change[1])
 
108
                        changes_shelved += 1
 
109
                if change[0] == 'delete file':
 
110
                    if self.prompt_bool('Shelve removing file "%s"?'
 
111
                                        % change[3]):
 
112
                        creator.shelve_deletion(change[1])
 
113
                        changes_shelved += 1
 
114
                if change[0] == 'change kind':
 
115
                    if self.prompt_bool('Shelve changing "%s" from %s to %s? '
 
116
                                        % (change[4], change[2], change[3])):
 
117
                        creator.shelve_content_change(change[1])
 
118
                        changes_shelved += 1
 
119
                if change[0] == 'rename':
 
120
                    if self.prompt_bool('Shelve renaming "%s" => "%s"?' %
 
121
                                   change[2:]):
 
122
                        creator.shelve_rename(change[1])
203
123
                        changes_shelved += 1
204
124
            if changes_shelved > 0:
205
 
                self.reporter.selected_changes(creator.work_transform)
 
125
                trace.note("Selected changes:")
 
126
                changes = creator.work_transform.iter_changes()
 
127
                reporter = delta._ChangeReporter()
 
128
                delta.report_changes(changes, reporter)
206
129
                if (self.auto_apply or self.prompt_bool(
207
 
                    self.reporter.vocab['final'] % changes_shelved)):
 
130
                    'Shelve %d change(s)?' % changes_shelved)):
208
131
                    if self.destroy:
209
132
                        creator.transform()
210
 
                        self.reporter.changes_destroyed()
 
133
                        trace.note('Selected changes destroyed.')
211
134
                    else:
212
135
                        shelf_id = self.manager.shelve_changes(creator,
213
136
                                                               self.message)
214
 
                        self.reporter.shelved_id(shelf_id)
 
137
                        trace.note('Changes shelved with id "%d".' % shelf_id)
215
138
            else:
216
 
                self.reporter.no_changes()
 
139
                trace.warning('No changes to shelve.')
217
140
        finally:
218
141
            shutil.rmtree(self.tempdir)
219
142
            creator.finalize()
220
143
 
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):
 
144
    def get_parsed_patch(self, file_id):
228
145
        """Return a parsed version of a file's patch.
229
146
 
230
147
        :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
148
        :return: A patches.Patch.
234
149
        """
 
150
        old_path = self.target_tree.id2path(file_id)
 
151
        new_path = self.work_tree.id2path(file_id)
235
152
        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())
 
153
        text_differ = diff.DiffText(self.target_tree, self.work_tree,
 
154
                                    diff_file)
246
155
        patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
247
156
        diff_file.seek(0)
248
157
        return patches.parse_patch(diff_file)
253
162
        :param message: The message to prompt a user with.
254
163
        :return: A character.
255
164
        """
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
165
        sys.stdout.write(message)
263
166
        char = osutils.getchar()
264
167
        sys.stdout.write("\r" + ' ' * len(message) + '\r')
265
168
        sys.stdout.flush()
266
169
        return char
267
170
 
268
 
    def prompt_bool(self, question, long=False, allow_editor=False):
 
171
    def prompt_bool(self, question, long=False):
269
172
        """Prompt the user with a yes/no question.
270
173
 
271
174
        This may be overridden by self.auto.  It may also *set* self.auto.  It
275
178
        """
276
179
        if self.auto:
277
180
            return True
278
 
        editor_string = ''
279
181
        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
 
182
            prompt = ' [(y)es, (N)o, (f)inish, or (q)uit]'
283
183
        else:
284
 
            if allow_editor:
285
 
                editor_string = 'e'
286
 
            prompt = ' [yN%sfq?]' % editor_string
 
184
            prompt = ' [yNfq?]'
287
185
        char = self.prompt(question + prompt)
288
186
        if char == 'y':
289
187
            return True
290
 
        elif char == 'e' and allow_editor:
291
 
            raise UseEditor
292
188
        elif char == 'f':
293
189
            self.auto = True
294
190
            return True
300
196
            return False
301
197
 
302
198
    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
199
        """Provide diff hunk selection for modified text.
321
200
 
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
201
        :param creator: a ShelfCreator
326
202
        :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
203
        :return: number of shelved hunks.
329
204
        """
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)
 
205
        target_lines = self.target_tree.get_file_lines(file_id)
 
206
        textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
335
207
        textfile.check_text_lines(target_lines)
336
 
        parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
 
208
        parsed = self.get_parsed_patch(file_id)
337
209
        final_hunks = []
338
210
        if not self.auto:
339
211
            offset = 0
340
212
            self.diff_writer.write(parsed.get_header())
341
213
            for hunk in parsed.hunks:
342
214
                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:
 
215
                if not self.prompt_bool('Shelve?'):
349
216
                    hunk.mod_pos += offset
350
217
                    final_hunks.append(hunk)
351
218
                else:
352
219
                    offset -= (hunk.mod_range - hunk.orig_range)
353
220
        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
 
221
        if len(parsed.hunks) == len(final_hunks):
 
222
            return 0
 
223
        patched = patches.iter_patched_from_hunks(target_lines, final_hunks)
 
224
        creator.shelve_lines(file_id, list(patched))
 
225
        return len(parsed.hunks) - len(final_hunks)
380
226
 
381
227
 
382
228
class Unshelver(object):
383
229
    """Unshelve changes into a working tree."""
384
230
 
385
231
    @classmethod
386
 
    def from_args(klass, shelf_id=None, action='apply', directory='.',
387
 
                  write_diff_to=None):
 
232
    def from_args(klass, shelf_id=None, action='apply', directory='.'):
388
233
        """Create an unshelver from commandline arguments.
389
234
 
390
 
        The returned shelver will have a tree that is locked and should
391
 
        be unlocked.
392
 
 
393
235
        :param shelf_id: Integer id of the shelf, as a string.
394
236
        :param action: action to perform.  May be 'apply', 'dry-run',
395
 
            'delete', 'preview'.
 
237
            'delete'.
396
238
        :param directory: The directory to unshelve changes into.
397
 
        :param write_diff_to: See Unshelver.__init__().
398
239
        """
399
240
        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
 
241
        manager = tree.get_shelf_manager()
 
242
        if shelf_id is not None:
 
243
            try:
 
244
                shelf_id = int(shelf_id)
 
245
            except ValueError:
 
246
                raise errors.InvalidShelfId(shelf_id)
 
247
        else:
 
248
            shelf_id = manager.last_shelf()
 
249
            if shelf_id is None:
 
250
                raise errors.BzrCommandError('No changes are shelved.')
 
251
            trace.note('Unshelving changes with id "%d".' % shelf_id)
 
252
        apply_changes = True
 
253
        delete_shelf = True
 
254
        read_shelf = True
 
255
        if action == 'dry-run':
 
256
            apply_changes = False
 
257
            delete_shelf = False
 
258
        if action == 'delete-only':
 
259
            apply_changes = False
 
260
            read_shelf = False
432
261
        return klass(tree, manager, shelf_id, apply_changes, delete_shelf,
433
 
                     read_shelf, show_diff, write_diff_to)
 
262
                     read_shelf)
434
263
 
435
264
    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):
 
265
                 delete_shelf=True, read_shelf=True):
438
266
        """Constructor.
439
267
 
440
268
        :param tree: The working tree to unshelve into.
444
272
            working tree.
445
273
        :param delete_shelf: If True, delete the changes from the shelf.
446
274
        :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
275
        """
453
276
        self.tree = tree
454
277
        manager = tree.get_shelf_manager()
457
280
        self.apply_changes = apply_changes
458
281
        self.delete_shelf = delete_shelf
459
282
        self.read_shelf = read_shelf
460
 
        self.show_diff = show_diff
461
 
        self.write_diff_to = write_diff_to
462
283
 
463
284
    def run(self):
464
285
        """Perform the unshelving operation."""
465
 
        self.tree.lock_tree_write()
 
286
        self.tree.lock_write()
466
287
        cleanups = [self.tree.unlock]
467
288
        try:
468
289
            if self.read_shelf:
469
 
                trace.note('Using changes with id "%d".' % self.shelf_id)
470
290
                unshelver = self.manager.get_unshelver(self.shelf_id)
471
291
                cleanups.append(unshelver.finalize)
472
292
                if unshelver.message is not None:
473
293
                    trace.note('Message: %s' % unshelver.message)
474
294
                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)
 
295
                task = ui.ui_factory.nested_progress_bar()
 
296
                try:
 
297
                    merger = unshelver.make_merger(task)
 
298
                    merger.change_reporter = change_reporter
 
299
                    if self.apply_changes:
 
300
                        merger.do_merge()
 
301
                    else:
 
302
                        self.show_changes(merger)
 
303
                finally:
 
304
                    task.finished()
483
305
            if self.delete_shelf:
484
306
                self.manager.delete_shelf(self.shelf_id)
485
 
                trace.note('Deleted changes with id "%d".' % self.shelf_id)
486
307
        finally:
487
308
            for cleanup in reversed(cleanups):
488
309
                cleanup()
489
310
 
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
311
    def show_changes(self, merger):
503
312
        """Show the changes that this operation specifies."""
504
313
        tree_merger = merger.make_merger()