Merge lp:~abentley/bzr/merge-interactive into lp:~bzr/bzr/trunk-old
- merge-interactive
- Merge into trunk-old
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Pool | Approve | ||
Review via email: mp+8705@code.launchpad.net |
Commit message
Description of the change
Aaron Bentley (abentley) wrote : | # |
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_
679 + tree = self.create_
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.
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_
> 679 + tree = self.create_
> 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://
iEYEARECAAYFAkp
EX4AnRpQjGzQuAG
=4eR1
-----END PGP SIGNATURE-----
Preview Diff
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): |
-----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 enigmail. mozdev. org
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://
iEYEARECAAYFAkp bc/8ACgkQ0F+ nu1YWqI093gCbBH NNWi/g5yt+ wHt/gps/ Y83b th1VioNq7dAsJri Ut
wlUAn1jNsmat6ZQ
=lKq+
-----END PGP SIGNATURE-----