Merge lp:~abentley/bzr/merge-interactive into lp:~bzr/bzr/trunk-old

Proposed by Aaron Bentley
Status: Merged
Merged at revision: not available
Proposed branch: lp:~abentley/bzr/merge-interactive
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 737 lines
To merge this branch: bzr merge lp:~abentley/bzr/merge-interactive
Reviewer Review Type Date Requested Status
Martin Pool Approve
Review via email: mp+8705@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Hi all,

This patch implements interactive merge, based on the shelf UI. To make
the prompts more intelligible, it introduces a new vocabulary for
shelving, the "apply" vocabulary.

The current set of prompts is described in terms of a reverting to the
basis. For example, if the working tree does not have a file, but the
target tree does, the user is assumed to have removed the file. They
are therefore asked if they want to "Shelve removing" the file. If they
say "yes", the file is added to the working tree.

When the target tree is not the basis tree, this vocabulary makes little
sense. The user has not removed the file, so why would they "shelve"
its "removal"? Instead, the "apply" vocabulary asks if they want to
"Add" the file.

For similar reasons, the diff hunks are inverted.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkpbc/8ACgkQ0F+nu1YWqI093gCbBHNNWi/g5yt+wHt/gps/Y83b
wlUAn1jNsmat6ZQth1VioNq7dAsJriUt
=lKq+
-----END PGP SIGNATURE-----

Revision history for this message
Martin Pool (mbp) wrote :

That's a really exciting feature, thanks for posting it.

It would be nice if it were mentioned in news and at least in the user manual to the extent of "you can do interactive merges with -i".

From the cover letter I thought you were saying merge would prompt with "Shelve removing..." but clearly it will actually say "Delete file %s".

I wonder if rather than having invert_diff=True and some of the messages backwards it could be a property of either this reporter or perhaps a parameter to Shelver that it's actually being applied as a negative. It just seems like the kind of thing that would easily be broken or just create an unnecessary puzzle or barrier to reuse.

For instance 'revert -i', which is really pretty close to shelve, might like to use this terminology.

You could have a decorator of the reporter that inverts it.

678 + def test_shelve_modify_target(self):
679 + tree = self.create_shelvable_tree()
680 + os.symlink('bar', 'tree/baz')

I think this needs to be guarded by having the symlink feature?

Perhaps a comment in do_interactive wouldn't go amiss: you're doing the whole merge into a preview tree, then iiuc selectively essentially reverting the working tree to look like the preview tree to bring those changes across. That's pretty elegant.

review: Approve
Revision history for this message
Aaron Bentley (abentley) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

Thanks for your review.

Martin Pool wrote:
> It would be nice if it were mentioned in news and at least in the user manual to the extent of "you can do interactive merges with -i".

I've updated NEWS, and added a short paragraph to the command help:

 To select only some changes to merge, use "merge -i", which will prompt
 you to apply each diff hunk and file change, similar to "shelve".

>>From the cover letter I thought you were saying merge would prompt with "Shelve removing..." but clearly it will actually say "Delete file %s".
>
> I wonder if rather than having invert_diff=True and some of the messages backwards it could be a property of either this reporter or perhaps a parameter to Shelver that it's actually being applied as a negative. It just seems like the kind of thing that would easily be broken or just create an unnecessary puzzle or barrier to reuse.

It's not really being applied as a negative. The same keystrokes have
exactly the same meaning as they did before. It's just that shelving a
removal is the same thing as applying an addition.

> For instance 'revert -i', which is really pretty close to shelve, might like to use this terminology.

Do you mean the "apply" terminology? That it will work as-is. If you
have deleted a file, then "revert -i" would prompt you to add it. It
should not prompt you to delete the file.

If you meant the "shelve" terminology, it will need to be a different
variant, updated to say "revert" everywhere the current reporter says
"shelve".

> 678 + def test_shelve_modify_target(self):
> 679 + tree = self.create_shelvable_tree()
> 680 + os.symlink('bar', 'tree/baz')
>
> I think this needs to be guarded by having the symlink feature?

Fixed.

>
> Perhaps a comment in do_interactive wouldn't go amiss: you're doing the whole merge into a preview tree, then iiuc selectively essentially reverting the working tree to look like the preview tree to bring those changes across. That's pretty elegant.

done.

I've submitted, but I'm happy to land a follow-on patch.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org

iEYEARECAAYFAkpcjwoACgkQ0F+nu1YWqI3+5QCfVGQEmVybFIDa2KtwaNyw022D
EX4AnRpQjGzQuAGUYK8dUt3IsRYDDLBF
=4eR1
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2009-07-13 13:25:18 +0000
3+++ NEWS 2009-07-14 14:35:13 +0000
4@@ -16,6 +16,9 @@
5 New Features
6 ************
7
8+* ``merge --interactive`` applies a user-selected portion of the merge. The UI
9+ is similar to ``shelve``. (Aaron Bentley)
10+
11 Bug Fixes
12 *********
13
14
15=== modified file 'bzrlib/builtins.py'
16--- bzrlib/builtins.py 2009-07-10 08:33:11 +0000
17+++ bzrlib/builtins.py 2009-07-14 14:35:13 +0000
18@@ -3573,6 +3573,9 @@
19 merge refuses to run if there are any uncommitted changes, unless
20 --force is given.
21
22+ To select only some changes to merge, use "merge -i", which will prompt
23+ you to apply each diff hunk and file change, similar to "shelve".
24+
25 :Examples:
26 To merge the latest revision from bzr.dev::
27
28@@ -3616,7 +3619,10 @@
29 short_name='d',
30 type=unicode,
31 ),
32- Option('preview', help='Instead of merging, show a diff of the merge.')
33+ Option('preview', help='Instead of merging, show a diff of the'
34+ ' merge.'),
35+ Option('interactive', help='Select changes interactively.',
36+ short_name='i')
37 ]
38
39 def run(self, location=None, revision=None, force=False,
40@@ -3624,6 +3630,7 @@
41 uncommitted=False, pull=False,
42 directory=None,
43 preview=False,
44+ interactive=False,
45 ):
46 if merge_type is None:
47 merge_type = _mod_merge.Merge3Merger
48@@ -3700,7 +3707,9 @@
49 return 0
50 merger.check_basis(False)
51 if preview:
52- return self._do_preview(merger)
53+ return self._do_preview(merger, cleanups)
54+ elif interactive:
55+ return self._do_interactive(merger, cleanups)
56 else:
57 return self._do_merge(merger, change_reporter, allow_pending,
58 verified)
59@@ -3708,16 +3717,18 @@
60 for cleanup in reversed(cleanups):
61 cleanup()
62
63- def _do_preview(self, merger):
64- from bzrlib.diff import show_diff_trees
65+ def _get_preview(self, merger, cleanups):
66 tree_merger = merger.make_merger()
67 tt = tree_merger.make_preview_transform()
68- try:
69- result_tree = tt.get_preview_tree()
70- show_diff_trees(merger.this_tree, result_tree, self.outf,
71- old_label='', new_label='')
72- finally:
73- tt.finalize()
74+ cleanups.append(tt.finalize)
75+ result_tree = tt.get_preview_tree()
76+ return result_tree
77+
78+ def _do_preview(self, merger, cleanups):
79+ from bzrlib.diff import show_diff_trees
80+ result_tree = self._get_preview(merger, cleanups)
81+ show_diff_trees(merger.this_tree, result_tree, self.outf,
82+ old_label='', new_label='')
83
84 def _do_merge(self, merger, change_reporter, allow_pending, verified):
85 merger.change_reporter = change_reporter
86@@ -3731,6 +3742,21 @@
87 else:
88 return 0
89
90+ def _do_interactive(self, merger, cleanups):
91+ """Perform an interactive merge.
92+
93+ This works by generating a preview tree of the merge, then using
94+ Shelver to selectively remove the differences between the working tree
95+ and the preview tree.
96+ """
97+ from bzrlib import shelf_ui
98+ result_tree = self._get_preview(merger, cleanups)
99+ writer = bzrlib.option.diff_writer_registry.get()
100+ shelver = shelf_ui.Shelver(merger.this_tree, result_tree, destroy=True,
101+ reporter=shelf_ui.ApplyReporter(),
102+ diff_writer=writer(sys.stdout))
103+ shelver.run()
104+
105 def sanity_check_merger(self, merger):
106 if (merger.show_base and
107 not merger.merge_type is _mod_merge.Merge3Merger):
108
109=== modified file 'bzrlib/shelf.py'
110--- bzrlib/shelf.py 2009-06-10 03:56:49 +0000
111+++ bzrlib/shelf.py 2009-07-14 14:35:13 +0000
112@@ -96,6 +96,21 @@
113 elif changed:
114 yield ('modify text', file_id)
115
116+ def shelve_change(self, change):
117+ """Shelve a change in the iter_shelvable format."""
118+ if change[0] == 'rename':
119+ self.shelve_rename(change[1])
120+ elif change[0] == 'delete file':
121+ self.shelve_deletion(change[1])
122+ elif change[0] == 'add file':
123+ self.shelve_creation(change[1])
124+ elif change[0] == 'change kind':
125+ self.shelve_content_change(change[1])
126+ elif change[0] == 'modify target':
127+ self.shelve_modify_target(change[1])
128+ else:
129+ raise ValueError('Unknown change kind: "%s"' % change[0])
130+
131 def shelve_rename(self, file_id):
132 """Shelve a file rename.
133
134
135=== modified file 'bzrlib/shelf_ui.py'
136--- bzrlib/shelf_ui.py 2009-06-26 03:44:30 +0000
137+++ bzrlib/shelf_ui.py 2009-07-14 14:35:13 +0000
138@@ -37,20 +37,75 @@
139
140 class ShelfReporter(object):
141
142+ vocab = {'add file': 'Shelve adding file "%(path)s"?',
143+ 'binary': 'Shelve binary changes?',
144+ 'change kind': 'Shelve changing "%s" from %(other)s'
145+ ' to %(this)s?',
146+ 'delete file': 'Shelve removing file "%(path)s"?',
147+ 'final': 'Shelve %d change(s)?',
148+ 'hunk': 'Shelve?',
149+ 'modify target': 'Shelve changing target of'
150+ ' "%(path)s" from "%(other)s" to "%(this)s"?',
151+ 'rename': 'Shelve renaming "%(other)s" =>'
152+ ' "%(this)s"?'
153+ }
154+
155+ invert_diff = False
156+
157 def __init__(self):
158 self.delta_reporter = delta._ChangeReporter()
159
160 def no_changes(self):
161+ """Report that no changes were selected to apply."""
162 trace.warning('No changes to shelve.')
163
164 def shelved_id(self, shelf_id):
165+ """Report the id changes were shelved to."""
166 trace.note('Changes shelved with id "%d".' % shelf_id)
167
168+ def changes_destroyed(self):
169+ """Report that changes were made without shelving."""
170+ trace.note('Selected changes destroyed.')
171+
172 def selected_changes(self, transform):
173+ """Report the changes that were selected."""
174 trace.note("Selected changes:")
175 changes = transform.iter_changes()
176 delta.report_changes(changes, self.delta_reporter)
177
178+ def prompt_change(self, change):
179+ """Determine the prompt for a change to apply."""
180+ if change[0] == 'rename':
181+ vals = {'this': change[3], 'other': change[2]}
182+ elif change[0] == 'change kind':
183+ vals = {'path': change[4], 'other': change[2], 'this': change[3]}
184+ elif change[0] == 'modify target':
185+ vals = {'path': change[2], 'other': change[3], 'this': change[4]}
186+ else:
187+ vals = {'path': change[3]}
188+ prompt = self.vocab[change[0]] % vals
189+ return prompt
190+
191+
192+class ApplyReporter(ShelfReporter):
193+
194+ vocab = {'add file': 'Delete file "%(path)s"?',
195+ 'binary': 'Apply binary changes?',
196+ 'change kind': 'Change "%(path)s" from %(this)s'
197+ ' to %(other)s?',
198+ 'delete file': 'Add file "%(path)s"?',
199+ 'final': 'Apply %d change(s)?',
200+ 'hunk': 'Apply change?',
201+ 'modify target': 'Change target of'
202+ ' "%(path)s" from "%(this)s" to "%(other)s"?',
203+ 'rename': 'Rename "%(this)s" => "%(other)s"?',
204+ }
205+
206+ invert_diff = True
207+
208+ def changes_destroyed(self):
209+ pass
210+
211
212 class Shelver(object):
213 """Interactively shelve the changes in a working tree."""
214@@ -70,6 +125,7 @@
215 :param destroy: Change the working tree without storing the shelved
216 changes.
217 :param manager: The shelf manager to use.
218+ :param reporter: Object for reporting changes to user.
219 """
220 self.work_tree = work_tree
221 self.target_tree = target_tree
222@@ -121,41 +177,20 @@
223 changes_shelved += self.handle_modify_text(creator,
224 change[1])
225 except errors.BinaryFile:
226- if self.prompt_bool('Shelve binary changes?'):
227+ if self.prompt_bool(self.reporter.vocab['binary']):
228 changes_shelved += 1
229 creator.shelve_content_change(change[1])
230- if change[0] == 'add file':
231- if self.prompt_bool('Shelve adding file "%s"?'
232- % change[3]):
233- creator.shelve_creation(change[1])
234- changes_shelved += 1
235- if change[0] == 'delete file':
236- if self.prompt_bool('Shelve removing file "%s"?'
237- % change[3]):
238- creator.shelve_deletion(change[1])
239- changes_shelved += 1
240- if change[0] == 'change kind':
241- if self.prompt_bool('Shelve changing "%s" from %s to %s? '
242- % (change[4], change[2], change[3])):
243- creator.shelve_content_change(change[1])
244- changes_shelved += 1
245- if change[0] == 'rename':
246- if self.prompt_bool('Shelve renaming "%s" => "%s"?' %
247- change[2:]):
248- creator.shelve_rename(change[1])
249- changes_shelved += 1
250- if change[0] == 'modify target':
251- if self.prompt_bool('Shelve changing target of "%s" '
252- 'from "%s" to "%s"?' % change[2:]):
253- creator.shelve_modify_target(change[1])
254+ else:
255+ if self.prompt_bool(self.reporter.prompt_change(change)):
256+ creator.shelve_change(change)
257 changes_shelved += 1
258 if changes_shelved > 0:
259 self.reporter.selected_changes(creator.work_transform)
260 if (self.auto_apply or self.prompt_bool(
261- 'Shelve %d change(s)?' % changes_shelved)):
262+ self.reporter.vocab['final'] % changes_shelved)):
263 if self.destroy:
264 creator.transform()
265- trace.note('Selected changes destroyed.')
266+ self.reporter.changes_destroyed()
267 else:
268 shelf_id = self.manager.shelve_changes(creator,
269 self.message)
270@@ -166,17 +201,24 @@
271 shutil.rmtree(self.tempdir)
272 creator.finalize()
273
274- def get_parsed_patch(self, file_id):
275+ def get_parsed_patch(self, file_id, invert=False):
276 """Return a parsed version of a file's patch.
277
278 :param file_id: The id of the file to generate a patch for.
279+ :param invert: If True, provide an inverted patch (insertions displayed
280+ as removals, removals displayed as insertions).
281 :return: A patches.Patch.
282 """
283- old_path = self.target_tree.id2path(file_id)
284- new_path = self.work_tree.id2path(file_id)
285 diff_file = StringIO()
286- text_differ = diff.DiffText(self.target_tree, self.work_tree,
287- diff_file)
288+ if invert:
289+ old_tree = self.work_tree
290+ new_tree = self.target_tree
291+ else:
292+ old_tree = self.target_tree
293+ new_tree = self.work_tree
294+ old_path = old_tree.id2path(file_id)
295+ new_path = new_tree.id2path(file_id)
296+ text_differ = diff.DiffText(old_tree, new_tree, diff_file)
297 patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
298 diff_file.seek(0)
299 return patches.parse_patch(diff_file)
300@@ -223,30 +265,44 @@
301 def handle_modify_text(self, creator, file_id):
302 """Provide diff hunk selection for modified text.
303
304+ If self.reporter.invert_diff is True, the diff is inverted so that
305+ insertions are displayed as removals and vice versa.
306+
307 :param creator: a ShelfCreator
308 :param file_id: The id of the file to shelve.
309 :return: number of shelved hunks.
310 """
311- target_lines = self.target_tree.get_file_lines(file_id)
312+ if self.reporter.invert_diff:
313+ target_lines = self.work_tree.get_file_lines(file_id)
314+ else:
315+ target_lines = self.target_tree.get_file_lines(file_id)
316 textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
317 textfile.check_text_lines(target_lines)
318- parsed = self.get_parsed_patch(file_id)
319+ parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
320 final_hunks = []
321 if not self.auto:
322 offset = 0
323 self.diff_writer.write(parsed.get_header())
324 for hunk in parsed.hunks:
325 self.diff_writer.write(str(hunk))
326- if not self.prompt_bool('Shelve?'):
327+ selected = self.prompt_bool(self.reporter.vocab['hunk'])
328+ if not self.reporter.invert_diff:
329+ selected = (not selected)
330+ if selected:
331 hunk.mod_pos += offset
332 final_hunks.append(hunk)
333 else:
334 offset -= (hunk.mod_range - hunk.orig_range)
335 sys.stdout.flush()
336- if len(parsed.hunks) == len(final_hunks):
337+ if not self.reporter.invert_diff and (
338+ len(parsed.hunks) == len(final_hunks)):
339+ return 0
340+ if self.reporter.invert_diff and len(final_hunks) == 0:
341 return 0
342 patched = patches.iter_patched_from_hunks(target_lines, final_hunks)
343 creator.shelve_lines(file_id, list(patched))
344+ if self.reporter.invert_diff:
345+ return len(final_hunks)
346 return len(parsed.hunks) - len(final_hunks)
347
348
349
350=== modified file 'bzrlib/tests/test_shelf.py'
351--- bzrlib/tests/test_shelf.py 2009-05-06 05:36:28 +0000
352+++ bzrlib/tests/test_shelf.py 2009-07-14 14:35:13 +0000
353@@ -40,7 +40,7 @@
354
355 class TestPrepareShelf(tests.TestCaseWithTransport):
356
357- def test_shelve_rename(self):
358+ def prepare_shelve_rename(self):
359 tree = self.make_branch_and_tree('.')
360 self.build_tree(['foo'])
361 tree.add(['foo'], ['foo-id'])
362@@ -50,7 +50,9 @@
363 self.addCleanup(creator.finalize)
364 self.assertEqual([('rename', 'foo-id', 'foo', 'bar')],
365 list(creator.iter_shelvable()))
366- creator.shelve_rename('foo-id')
367+ return creator
368+
369+ def check_shelve_rename(self, creator):
370 work_trans_id = creator.work_transform.trans_id_file_id('foo-id')
371 self.assertEqual('foo', creator.work_transform.final_name(
372 work_trans_id))
373@@ -58,7 +60,17 @@
374 self.assertEqual('bar', creator.shelf_transform.final_name(
375 shelf_trans_id))
376
377- def test_shelve_move(self):
378+ def test_shelve_rename(self):
379+ creator = self.prepare_shelve_rename()
380+ creator.shelve_rename('foo-id')
381+ self.check_shelve_rename(creator)
382+
383+ def test_shelve_change_handles_rename(self):
384+ creator = self.prepare_shelve_rename()
385+ creator.shelve_change(('rename', 'foo-id', 'foo', 'bar'))
386+ self.check_shelve_rename(creator)
387+
388+ def prepare_shelve_move(self):
389 tree = self.make_branch_and_tree('.')
390 self.build_tree(['foo/', 'bar/', 'foo/baz'])
391 tree.add(['foo', 'bar', 'foo/baz'], ['foo-id', 'bar-id', 'baz-id'])
392@@ -68,7 +80,9 @@
393 self.addCleanup(creator.finalize)
394 self.assertEqual([('rename', 'baz-id', 'foo/baz', 'bar/baz')],
395 list(creator.iter_shelvable()))
396- creator.shelve_rename('baz-id')
397+ return creator, tree
398+
399+ def check_shelve_move(self, creator, tree):
400 work_trans_id = creator.work_transform.trans_id_file_id('baz-id')
401 work_foo = creator.work_transform.trans_id_file_id('foo-id')
402 self.assertEqual(work_foo, creator.work_transform.final_parent(
403@@ -80,6 +94,16 @@
404 creator.transform()
405 self.assertEqual('foo/baz', tree.id2path('baz-id'))
406
407+ def test_shelve_move(self):
408+ creator, tree = self.prepare_shelve_move()
409+ creator.shelve_rename('baz-id')
410+ self.check_shelve_move(creator, tree)
411+
412+ def test_shelve_change_handles_move(self):
413+ creator, tree = self.prepare_shelve_move()
414+ creator.shelve_change(('rename', 'baz-id', 'foo/baz', 'bar/baz'))
415+ self.check_shelve_move(creator, tree)
416+
417 def assertShelvedFileEqual(self, expected_content, creator, file_id):
418 s_trans_id = creator.shelf_transform.trans_id_file_id(file_id)
419 shelf_file = creator.shelf_transform._limbo_name(s_trans_id)
420@@ -102,7 +126,8 @@
421 self.assertFileEqual('a\nc\n', 'foo')
422 self.assertShelvedFileEqual('b\na\n', creator, 'foo-id')
423
424- def test_shelve_creation(self):
425+
426+ def prepare_shelve_creation(self):
427 tree = self.make_branch_and_tree('.')
428 tree.lock_write()
429 self.addCleanup(tree.unlock)
430@@ -114,9 +139,9 @@
431 self.assertEqual([('add file', 'bar-id', 'directory', 'bar'),
432 ('add file', 'foo-id', 'file', 'foo')],
433 sorted(list(creator.iter_shelvable())))
434- creator.shelve_creation('foo-id')
435- creator.shelve_creation('bar-id')
436- creator.transform()
437+ return creator, tree
438+
439+ def check_shelve_creation(self, creator, tree):
440 self.assertRaises(StopIteration,
441 tree.iter_entries_by_dir(['foo-id']).next)
442 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
443@@ -129,7 +154,22 @@
444 self.assertEqual('directory',
445 creator.shelf_transform.final_kind(s_bar_trans_id))
446
447- def _test_shelve_symlink_creation(self, link_name, link_target):
448+ def test_shelve_creation(self):
449+ creator, tree = self.prepare_shelve_creation()
450+ creator.shelve_creation('foo-id')
451+ creator.shelve_creation('bar-id')
452+ creator.transform()
453+ self.check_shelve_creation(creator, tree)
454+
455+ def test_shelve_change_handles_creation(self):
456+ creator, tree = self.prepare_shelve_creation()
457+ creator.shelve_change(('add file', 'foo-id', 'file', 'foo'))
458+ creator.shelve_change(('add file', 'bar-id', 'directory', 'bar'))
459+ creator.transform()
460+ self.check_shelve_creation(creator, tree)
461+
462+ def _test_shelve_symlink_creation(self, link_name, link_target,
463+ shelve_change=False):
464 self.requireFeature(tests.SymlinkFeature)
465 tree = self.make_branch_and_tree('.')
466 tree.lock_write()
467@@ -141,7 +181,10 @@
468 self.addCleanup(creator.finalize)
469 self.assertEqual([('add file', 'foo-id', 'symlink', link_name)],
470 list(creator.iter_shelvable()))
471- creator.shelve_creation('foo-id')
472+ if shelve_change:
473+ creator.shelve_change(('add file', 'foo-id', 'symlink', link_name))
474+ else:
475+ creator.shelve_creation('foo-id')
476 creator.transform()
477 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
478 self.failIfExists(link_name)
479@@ -158,8 +201,12 @@
480 self._test_shelve_symlink_creation(u'fo\N{Euro Sign}o',
481 u'b\N{Euro Sign}ar')
482
483+ def test_shelve_change_handles_symlink_creation(self):
484+ self._test_shelve_symlink_creation('foo', 'bar', shelve_change=True)
485+
486 def _test_shelve_symlink_target_change(self, link_name,
487- old_target, new_target):
488+ old_target, new_target,
489+ shelve_change=False):
490 self.requireFeature(tests.SymlinkFeature)
491 tree = self.make_branch_and_tree('.')
492 tree.lock_write()
493@@ -174,7 +221,11 @@
494 self.assertEqual([('modify target', 'foo-id', link_name,
495 old_target, new_target)],
496 list(creator.iter_shelvable()))
497- creator.shelve_modify_target('foo-id')
498+ if shelve_change:
499+ creator.shelve_change(('modify target', 'foo-id', link_name,
500+ old_target, new_target))
501+ else:
502+ creator.shelve_modify_target('foo-id')
503 creator.transform()
504 self.assertEqual(old_target, osutils.readlink(link_name))
505 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
506@@ -191,6 +242,10 @@
507 self._test_shelve_symlink_target_change(
508 u'fo\N{Euro Sign}o', u'b\N{Euro Sign}ar', u'b\N{Euro Sign}az')
509
510+ def test_shelve_change_handles_symlink_target_change(self):
511+ self._test_shelve_symlink_target_change('foo', 'bar', 'baz',
512+ shelve_change=True)
513+
514 def test_shelve_creation_no_contents(self):
515 tree = self.make_branch_and_tree('.')
516 tree.lock_write()
517@@ -213,7 +268,7 @@
518 creator.shelf_transform.final_file_id(s_trans_id))
519 self.failIfExists('foo')
520
521- def test_shelve_deletion(self):
522+ def prepare_shelve_deletion(self):
523 tree = self.make_branch_and_tree('tree')
524 tree.lock_write()
525 self.addCleanup(tree.unlock)
526@@ -228,13 +283,27 @@
527 self.assertEqual([('delete file', 'bar-id', 'file', 'foo/bar'),
528 ('delete file', 'foo-id', 'directory', 'foo')],
529 sorted(list(creator.iter_shelvable())))
530- creator.shelve_deletion('foo-id')
531- creator.shelve_deletion('bar-id')
532- creator.transform()
533+ return creator, tree
534+
535+ def check_shelve_deletion(self, tree):
536 self.assertTrue('foo-id' in tree)
537 self.assertTrue('bar-id' in tree)
538 self.assertFileEqual('baz', 'tree/foo/bar')
539
540+ def test_shelve_deletion(self):
541+ creator, tree = self.prepare_shelve_deletion()
542+ creator.shelve_deletion('foo-id')
543+ creator.shelve_deletion('bar-id')
544+ creator.transform()
545+ self.check_shelve_deletion(tree)
546+
547+ def test_shelve_change_handles_deletion(self):
548+ creator, tree = self.prepare_shelve_deletion()
549+ creator.shelve_change(('delete file', 'foo-id', 'directory', 'foo'))
550+ creator.shelve_change(('delete file', 'bar-id', 'file', 'foo/bar'))
551+ creator.transform()
552+ self.check_shelve_deletion(tree)
553+
554 def test_shelve_delete_contents(self):
555 tree = self.make_branch_and_tree('tree')
556 self.build_tree(['tree/foo',])
557@@ -249,7 +318,7 @@
558 creator.transform()
559 self.failUnlessExists('tree/foo')
560
561- def test_shelve_change_kind(self):
562+ def prepare_shelve_change_kind(self):
563 tree = self.make_branch_and_tree('tree')
564 self.build_tree_contents([('tree/foo', 'bar')])
565 tree.add('foo', 'foo-id')
566@@ -260,12 +329,33 @@
567 self.addCleanup(creator.finalize)
568 self.assertEqual([('change kind', 'foo-id', 'file', 'directory',
569 'foo')], sorted(list(creator.iter_shelvable())))
570+ return creator
571+
572+ def check_shelve_change_kind(self, creator):
573+ self.assertFileEqual('bar', 'tree/foo')
574+ s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
575+ self.assertEqual('directory',
576+ creator.shelf_transform._new_contents[s_trans_id])
577+
578+ def test_shelve_change_kind(self):
579+ creator = self.prepare_shelve_change_kind()
580 creator.shelve_content_change('foo-id')
581 creator.transform()
582- self.assertFileEqual('bar', 'tree/foo')
583- s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
584- self.assertEqual('directory',
585- creator.shelf_transform._new_contents[s_trans_id])
586+ self.check_shelve_change_kind(creator)
587+
588+ def test_shelve_change_handles_change_kind(self):
589+ creator = self.prepare_shelve_change_kind()
590+ creator.shelve_change(('change kind', 'foo-id', 'file', 'directory',
591+ 'foo'))
592+ creator.transform()
593+ self.check_shelve_change_kind(creator)
594+
595+ def test_shelve_change_unknown_change(self):
596+ tree = self.make_branch_and_tree('tree')
597+ creator = shelf.ShelfCreator(tree, tree.basis_tree())
598+ self.addCleanup(creator.finalize)
599+ e = self.assertRaises(ValueError, creator.shelve_change, ('unknown',))
600+ self.assertEqual('Unknown change kind: "unknown"', str(e))
601
602 def test_shelve_unversion(self):
603 tree = self.make_branch_and_tree('tree')
604
605=== modified file 'bzrlib/tests/test_shelf_ui.py'
606--- bzrlib/tests/test_shelf_ui.py 2009-03-23 14:59:43 +0000
607+++ bzrlib/tests/test_shelf_ui.py 2009-07-14 14:35:13 +0000
608@@ -27,10 +27,10 @@
609
610 def __init__(self, work_tree, target_tree, diff_writer=None,
611 auto=False, auto_apply=False, file_list=None, message=None,
612- destroy=False):
613+ destroy=False, reporter=None):
614 shelf_ui.Shelver.__init__(self, work_tree, target_tree, diff_writer,
615 auto, auto_apply, file_list, message,
616- destroy)
617+ destroy, reporter=reporter)
618 self.expected = []
619 self.diff_writer = StringIO()
620
621@@ -165,6 +165,7 @@
622 shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
623
624 def test_shelve_modify_target(self):
625+ self.requireFeature(tests.SymlinkFeature)
626 tree = self.create_shelvable_tree()
627 os.symlink('bar', 'tree/baz')
628 tree.add('baz', 'baz-id')
629@@ -224,6 +225,108 @@
630 self.assertFileEqual(LINES_AJ, 'tree/foo')
631
632
633+class TestApplyReporter(TestShelver):
634+
635+ def test_shelve_not_diff(self):
636+ tree = self.create_shelvable_tree()
637+ shelver = ExpectShelver(tree, tree.basis_tree(),
638+ reporter=shelf_ui.ApplyReporter())
639+ shelver.expect('Apply change? [yNfq?]', 'n')
640+ shelver.expect('Apply change? [yNfq?]', 'n')
641+ # No final shelving prompt because no changes were selected
642+ shelver.run()
643+ self.assertFileEqual(LINES_ZY, 'tree/foo')
644+
645+ def test_shelve_diff_no(self):
646+ tree = self.create_shelvable_tree()
647+ shelver = ExpectShelver(tree, tree.basis_tree(),
648+ reporter=shelf_ui.ApplyReporter())
649+ shelver.expect('Apply change? [yNfq?]', 'y')
650+ shelver.expect('Apply change? [yNfq?]', 'y')
651+ shelver.expect('Apply 2 change(s)? [yNfq?]', 'n')
652+ shelver.run()
653+ self.assertFileEqual(LINES_ZY, 'tree/foo')
654+
655+ def test_shelve_diff(self):
656+ tree = self.create_shelvable_tree()
657+ shelver = ExpectShelver(tree, tree.basis_tree(),
658+ reporter=shelf_ui.ApplyReporter())
659+ shelver.expect('Apply change? [yNfq?]', 'y')
660+ shelver.expect('Apply change? [yNfq?]', 'y')
661+ shelver.expect('Apply 2 change(s)? [yNfq?]', 'y')
662+ shelver.run()
663+ self.assertFileEqual(LINES_AJ, 'tree/foo')
664+
665+ def test_shelve_binary_change(self):
666+ tree = self.create_shelvable_tree()
667+ self.build_tree_contents([('tree/foo', '\x00')])
668+ shelver = ExpectShelver(tree, tree.basis_tree(),
669+ reporter=shelf_ui.ApplyReporter())
670+ shelver.expect('Apply binary changes? [yNfq?]', 'y')
671+ shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
672+ shelver.run()
673+ self.assertFileEqual(LINES_AJ, 'tree/foo')
674+
675+ def test_shelve_rename(self):
676+ tree = self.create_shelvable_tree()
677+ tree.rename_one('foo', 'bar')
678+ shelver = ExpectShelver(tree, tree.basis_tree(),
679+ reporter=shelf_ui.ApplyReporter())
680+ shelver.expect('Rename "bar" => "foo"? [yNfq?]', 'y')
681+ shelver.expect('Apply change? [yNfq?]', 'y')
682+ shelver.expect('Apply change? [yNfq?]', 'y')
683+ shelver.expect('Apply 3 change(s)? [yNfq?]', 'y')
684+ shelver.run()
685+ self.assertFileEqual(LINES_AJ, 'tree/foo')
686+
687+ def test_shelve_deletion(self):
688+ tree = self.create_shelvable_tree()
689+ os.unlink('tree/foo')
690+ shelver = ExpectShelver(tree, tree.basis_tree(),
691+ reporter=shelf_ui.ApplyReporter())
692+ shelver.expect('Add file "foo"? [yNfq?]', 'y')
693+ shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
694+ shelver.run()
695+ self.assertFileEqual(LINES_AJ, 'tree/foo')
696+
697+ def test_shelve_creation(self):
698+ tree = self.make_branch_and_tree('tree')
699+ tree.commit('add tree root')
700+ self.build_tree(['tree/foo'])
701+ tree.add('foo')
702+ shelver = ExpectShelver(tree, tree.basis_tree(),
703+ reporter=shelf_ui.ApplyReporter())
704+ shelver.expect('Delete file "foo"? [yNfq?]', 'y')
705+ shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
706+ shelver.run()
707+ self.failIfExists('tree/foo')
708+
709+ def test_shelve_kind_change(self):
710+ tree = self.create_shelvable_tree()
711+ os.unlink('tree/foo')
712+ os.mkdir('tree/foo')
713+ shelver = ExpectShelver(tree, tree.basis_tree(),
714+ reporter=shelf_ui.ApplyReporter())
715+ shelver.expect('Change "foo" from directory to a file? [yNfq?]', 'y')
716+ shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
717+
718+ def test_shelve_modify_target(self):
719+ self.requireFeature(tests.SymlinkFeature)
720+ tree = self.create_shelvable_tree()
721+ os.symlink('bar', 'tree/baz')
722+ tree.add('baz', 'baz-id')
723+ tree.commit("Add symlink")
724+ os.unlink('tree/baz')
725+ os.symlink('vax', 'tree/baz')
726+ shelver = ExpectShelver(tree, tree.basis_tree(),
727+ reporter=shelf_ui.ApplyReporter())
728+ shelver.expect('Change target of "baz" from "vax" to "bar"? [yNfq?]',
729+ 'y')
730+ shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
731+ shelver.run()
732+ self.assertEqual('bar', os.readlink('tree/baz'))
733+
734+
735 class TestUnshelver(tests.TestCaseWithTransport):
736
737 def create_tree_with_shelf(self):