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
=== modified file 'NEWS'
--- NEWS 2009-07-13 13:25:18 +0000
+++ NEWS 2009-07-14 14:35:13 +0000
@@ -16,6 +16,9 @@
16New Features16New Features
17************17************
1818
19* ``merge --interactive`` applies a user-selected portion of the merge. The UI
20 is similar to ``shelve``. (Aaron Bentley)
21
19Bug Fixes22Bug Fixes
20*********23*********
2124
2225
=== modified file 'bzrlib/builtins.py'
--- bzrlib/builtins.py 2009-07-10 08:33:11 +0000
+++ bzrlib/builtins.py 2009-07-14 14:35:13 +0000
@@ -3573,6 +3573,9 @@
3573 merge refuses to run if there are any uncommitted changes, unless3573 merge refuses to run if there are any uncommitted changes, unless
3574 --force is given.3574 --force is given.
35753575
3576 To select only some changes to merge, use "merge -i", which will prompt
3577 you to apply each diff hunk and file change, similar to "shelve".
3578
3576 :Examples:3579 :Examples:
3577 To merge the latest revision from bzr.dev::3580 To merge the latest revision from bzr.dev::
35783581
@@ -3616,7 +3619,10 @@
3616 short_name='d',3619 short_name='d',
3617 type=unicode,3620 type=unicode,
3618 ),3621 ),
3619 Option('preview', help='Instead of merging, show a diff of the merge.')3622 Option('preview', help='Instead of merging, show a diff of the'
3623 ' merge.'),
3624 Option('interactive', help='Select changes interactively.',
3625 short_name='i')
3620 ]3626 ]
36213627
3622 def run(self, location=None, revision=None, force=False,3628 def run(self, location=None, revision=None, force=False,
@@ -3624,6 +3630,7 @@
3624 uncommitted=False, pull=False,3630 uncommitted=False, pull=False,
3625 directory=None,3631 directory=None,
3626 preview=False,3632 preview=False,
3633 interactive=False,
3627 ):3634 ):
3628 if merge_type is None:3635 if merge_type is None:
3629 merge_type = _mod_merge.Merge3Merger3636 merge_type = _mod_merge.Merge3Merger
@@ -3700,7 +3707,9 @@
3700 return 03707 return 0
3701 merger.check_basis(False)3708 merger.check_basis(False)
3702 if preview:3709 if preview:
3703 return self._do_preview(merger)3710 return self._do_preview(merger, cleanups)
3711 elif interactive:
3712 return self._do_interactive(merger, cleanups)
3704 else:3713 else:
3705 return self._do_merge(merger, change_reporter, allow_pending,3714 return self._do_merge(merger, change_reporter, allow_pending,
3706 verified)3715 verified)
@@ -3708,16 +3717,18 @@
3708 for cleanup in reversed(cleanups):3717 for cleanup in reversed(cleanups):
3709 cleanup()3718 cleanup()
37103719
3711 def _do_preview(self, merger):3720 def _get_preview(self, merger, cleanups):
3712 from bzrlib.diff import show_diff_trees
3713 tree_merger = merger.make_merger()3721 tree_merger = merger.make_merger()
3714 tt = tree_merger.make_preview_transform()3722 tt = tree_merger.make_preview_transform()
3715 try:3723 cleanups.append(tt.finalize)
3716 result_tree = tt.get_preview_tree()3724 result_tree = tt.get_preview_tree()
3717 show_diff_trees(merger.this_tree, result_tree, self.outf,3725 return result_tree
3718 old_label='', new_label='')3726
3719 finally:3727 def _do_preview(self, merger, cleanups):
3720 tt.finalize()3728 from bzrlib.diff import show_diff_trees
3729 result_tree = self._get_preview(merger, cleanups)
3730 show_diff_trees(merger.this_tree, result_tree, self.outf,
3731 old_label='', new_label='')
37213732
3722 def _do_merge(self, merger, change_reporter, allow_pending, verified):3733 def _do_merge(self, merger, change_reporter, allow_pending, verified):
3723 merger.change_reporter = change_reporter3734 merger.change_reporter = change_reporter
@@ -3731,6 +3742,21 @@
3731 else:3742 else:
3732 return 03743 return 0
37333744
3745 def _do_interactive(self, merger, cleanups):
3746 """Perform an interactive merge.
3747
3748 This works by generating a preview tree of the merge, then using
3749 Shelver to selectively remove the differences between the working tree
3750 and the preview tree.
3751 """
3752 from bzrlib import shelf_ui
3753 result_tree = self._get_preview(merger, cleanups)
3754 writer = bzrlib.option.diff_writer_registry.get()
3755 shelver = shelf_ui.Shelver(merger.this_tree, result_tree, destroy=True,
3756 reporter=shelf_ui.ApplyReporter(),
3757 diff_writer=writer(sys.stdout))
3758 shelver.run()
3759
3734 def sanity_check_merger(self, merger):3760 def sanity_check_merger(self, merger):
3735 if (merger.show_base and3761 if (merger.show_base and
3736 not merger.merge_type is _mod_merge.Merge3Merger):3762 not merger.merge_type is _mod_merge.Merge3Merger):
37373763
=== modified file 'bzrlib/shelf.py'
--- bzrlib/shelf.py 2009-06-10 03:56:49 +0000
+++ bzrlib/shelf.py 2009-07-14 14:35:13 +0000
@@ -96,6 +96,21 @@
96 elif changed:96 elif changed:
97 yield ('modify text', file_id)97 yield ('modify text', file_id)
9898
99 def shelve_change(self, change):
100 """Shelve a change in the iter_shelvable format."""
101 if change[0] == 'rename':
102 self.shelve_rename(change[1])
103 elif change[0] == 'delete file':
104 self.shelve_deletion(change[1])
105 elif change[0] == 'add file':
106 self.shelve_creation(change[1])
107 elif change[0] == 'change kind':
108 self.shelve_content_change(change[1])
109 elif change[0] == 'modify target':
110 self.shelve_modify_target(change[1])
111 else:
112 raise ValueError('Unknown change kind: "%s"' % change[0])
113
99 def shelve_rename(self, file_id):114 def shelve_rename(self, file_id):
100 """Shelve a file rename.115 """Shelve a file rename.
101116
102117
=== modified file 'bzrlib/shelf_ui.py'
--- bzrlib/shelf_ui.py 2009-06-26 03:44:30 +0000
+++ bzrlib/shelf_ui.py 2009-07-14 14:35:13 +0000
@@ -37,20 +37,75 @@
3737
38class ShelfReporter(object):38class ShelfReporter(object):
3939
40 vocab = {'add file': 'Shelve adding file "%(path)s"?',
41 'binary': 'Shelve binary changes?',
42 'change kind': 'Shelve changing "%s" from %(other)s'
43 ' to %(this)s?',
44 'delete file': 'Shelve removing file "%(path)s"?',
45 'final': 'Shelve %d change(s)?',
46 'hunk': 'Shelve?',
47 'modify target': 'Shelve changing target of'
48 ' "%(path)s" from "%(other)s" to "%(this)s"?',
49 'rename': 'Shelve renaming "%(other)s" =>'
50 ' "%(this)s"?'
51 }
52
53 invert_diff = False
54
40 def __init__(self):55 def __init__(self):
41 self.delta_reporter = delta._ChangeReporter()56 self.delta_reporter = delta._ChangeReporter()
4257
43 def no_changes(self):58 def no_changes(self):
59 """Report that no changes were selected to apply."""
44 trace.warning('No changes to shelve.')60 trace.warning('No changes to shelve.')
4561
46 def shelved_id(self, shelf_id):62 def shelved_id(self, shelf_id):
63 """Report the id changes were shelved to."""
47 trace.note('Changes shelved with id "%d".' % shelf_id)64 trace.note('Changes shelved with id "%d".' % shelf_id)
4865
66 def changes_destroyed(self):
67 """Report that changes were made without shelving."""
68 trace.note('Selected changes destroyed.')
69
49 def selected_changes(self, transform):70 def selected_changes(self, transform):
71 """Report the changes that were selected."""
50 trace.note("Selected changes:")72 trace.note("Selected changes:")
51 changes = transform.iter_changes()73 changes = transform.iter_changes()
52 delta.report_changes(changes, self.delta_reporter)74 delta.report_changes(changes, self.delta_reporter)
5375
76 def prompt_change(self, change):
77 """Determine the prompt for a change to apply."""
78 if change[0] == 'rename':
79 vals = {'this': change[3], 'other': change[2]}
80 elif change[0] == 'change kind':
81 vals = {'path': change[4], 'other': change[2], 'this': change[3]}
82 elif change[0] == 'modify target':
83 vals = {'path': change[2], 'other': change[3], 'this': change[4]}
84 else:
85 vals = {'path': change[3]}
86 prompt = self.vocab[change[0]] % vals
87 return prompt
88
89
90class ApplyReporter(ShelfReporter):
91
92 vocab = {'add file': 'Delete file "%(path)s"?',
93 'binary': 'Apply binary changes?',
94 'change kind': 'Change "%(path)s" from %(this)s'
95 ' to %(other)s?',
96 'delete file': 'Add file "%(path)s"?',
97 'final': 'Apply %d change(s)?',
98 'hunk': 'Apply change?',
99 'modify target': 'Change target of'
100 ' "%(path)s" from "%(this)s" to "%(other)s"?',
101 'rename': 'Rename "%(this)s" => "%(other)s"?',
102 }
103
104 invert_diff = True
105
106 def changes_destroyed(self):
107 pass
108
54109
55class Shelver(object):110class Shelver(object):
56 """Interactively shelve the changes in a working tree."""111 """Interactively shelve the changes in a working tree."""
@@ -70,6 +125,7 @@
70 :param destroy: Change the working tree without storing the shelved125 :param destroy: Change the working tree without storing the shelved
71 changes.126 changes.
72 :param manager: The shelf manager to use.127 :param manager: The shelf manager to use.
128 :param reporter: Object for reporting changes to user.
73 """129 """
74 self.work_tree = work_tree130 self.work_tree = work_tree
75 self.target_tree = target_tree131 self.target_tree = target_tree
@@ -121,41 +177,20 @@
121 changes_shelved += self.handle_modify_text(creator,177 changes_shelved += self.handle_modify_text(creator,
122 change[1])178 change[1])
123 except errors.BinaryFile:179 except errors.BinaryFile:
124 if self.prompt_bool('Shelve binary changes?'):180 if self.prompt_bool(self.reporter.vocab['binary']):
125 changes_shelved += 1181 changes_shelved += 1
126 creator.shelve_content_change(change[1])182 creator.shelve_content_change(change[1])
127 if change[0] == 'add file':183 else:
128 if self.prompt_bool('Shelve adding file "%s"?'184 if self.prompt_bool(self.reporter.prompt_change(change)):
129 % change[3]):185 creator.shelve_change(change)
130 creator.shelve_creation(change[1])
131 changes_shelved += 1
132 if change[0] == 'delete file':
133 if self.prompt_bool('Shelve removing file "%s"?'
134 % change[3]):
135 creator.shelve_deletion(change[1])
136 changes_shelved += 1
137 if change[0] == 'change kind':
138 if self.prompt_bool('Shelve changing "%s" from %s to %s? '
139 % (change[4], change[2], change[3])):
140 creator.shelve_content_change(change[1])
141 changes_shelved += 1
142 if change[0] == 'rename':
143 if self.prompt_bool('Shelve renaming "%s" => "%s"?' %
144 change[2:]):
145 creator.shelve_rename(change[1])
146 changes_shelved += 1
147 if change[0] == 'modify target':
148 if self.prompt_bool('Shelve changing target of "%s" '
149 'from "%s" to "%s"?' % change[2:]):
150 creator.shelve_modify_target(change[1])
151 changes_shelved += 1186 changes_shelved += 1
152 if changes_shelved > 0:187 if changes_shelved > 0:
153 self.reporter.selected_changes(creator.work_transform)188 self.reporter.selected_changes(creator.work_transform)
154 if (self.auto_apply or self.prompt_bool(189 if (self.auto_apply or self.prompt_bool(
155 'Shelve %d change(s)?' % changes_shelved)):190 self.reporter.vocab['final'] % changes_shelved)):
156 if self.destroy:191 if self.destroy:
157 creator.transform()192 creator.transform()
158 trace.note('Selected changes destroyed.')193 self.reporter.changes_destroyed()
159 else:194 else:
160 shelf_id = self.manager.shelve_changes(creator,195 shelf_id = self.manager.shelve_changes(creator,
161 self.message)196 self.message)
@@ -166,17 +201,24 @@
166 shutil.rmtree(self.tempdir)201 shutil.rmtree(self.tempdir)
167 creator.finalize()202 creator.finalize()
168203
169 def get_parsed_patch(self, file_id):204 def get_parsed_patch(self, file_id, invert=False):
170 """Return a parsed version of a file's patch.205 """Return a parsed version of a file's patch.
171206
172 :param file_id: The id of the file to generate a patch for.207 :param file_id: The id of the file to generate a patch for.
208 :param invert: If True, provide an inverted patch (insertions displayed
209 as removals, removals displayed as insertions).
173 :return: A patches.Patch.210 :return: A patches.Patch.
174 """211 """
175 old_path = self.target_tree.id2path(file_id)
176 new_path = self.work_tree.id2path(file_id)
177 diff_file = StringIO()212 diff_file = StringIO()
178 text_differ = diff.DiffText(self.target_tree, self.work_tree,213 if invert:
179 diff_file)214 old_tree = self.work_tree
215 new_tree = self.target_tree
216 else:
217 old_tree = self.target_tree
218 new_tree = self.work_tree
219 old_path = old_tree.id2path(file_id)
220 new_path = new_tree.id2path(file_id)
221 text_differ = diff.DiffText(old_tree, new_tree, diff_file)
180 patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')222 patch = text_differ.diff(file_id, old_path, new_path, 'file', 'file')
181 diff_file.seek(0)223 diff_file.seek(0)
182 return patches.parse_patch(diff_file)224 return patches.parse_patch(diff_file)
@@ -223,30 +265,44 @@
223 def handle_modify_text(self, creator, file_id):265 def handle_modify_text(self, creator, file_id):
224 """Provide diff hunk selection for modified text.266 """Provide diff hunk selection for modified text.
225267
268 If self.reporter.invert_diff is True, the diff is inverted so that
269 insertions are displayed as removals and vice versa.
270
226 :param creator: a ShelfCreator271 :param creator: a ShelfCreator
227 :param file_id: The id of the file to shelve.272 :param file_id: The id of the file to shelve.
228 :return: number of shelved hunks.273 :return: number of shelved hunks.
229 """274 """
230 target_lines = self.target_tree.get_file_lines(file_id)275 if self.reporter.invert_diff:
276 target_lines = self.work_tree.get_file_lines(file_id)
277 else:
278 target_lines = self.target_tree.get_file_lines(file_id)
231 textfile.check_text_lines(self.work_tree.get_file_lines(file_id))279 textfile.check_text_lines(self.work_tree.get_file_lines(file_id))
232 textfile.check_text_lines(target_lines)280 textfile.check_text_lines(target_lines)
233 parsed = self.get_parsed_patch(file_id)281 parsed = self.get_parsed_patch(file_id, self.reporter.invert_diff)
234 final_hunks = []282 final_hunks = []
235 if not self.auto:283 if not self.auto:
236 offset = 0284 offset = 0
237 self.diff_writer.write(parsed.get_header())285 self.diff_writer.write(parsed.get_header())
238 for hunk in parsed.hunks:286 for hunk in parsed.hunks:
239 self.diff_writer.write(str(hunk))287 self.diff_writer.write(str(hunk))
240 if not self.prompt_bool('Shelve?'):288 selected = self.prompt_bool(self.reporter.vocab['hunk'])
289 if not self.reporter.invert_diff:
290 selected = (not selected)
291 if selected:
241 hunk.mod_pos += offset292 hunk.mod_pos += offset
242 final_hunks.append(hunk)293 final_hunks.append(hunk)
243 else:294 else:
244 offset -= (hunk.mod_range - hunk.orig_range)295 offset -= (hunk.mod_range - hunk.orig_range)
245 sys.stdout.flush()296 sys.stdout.flush()
246 if len(parsed.hunks) == len(final_hunks):297 if not self.reporter.invert_diff and (
298 len(parsed.hunks) == len(final_hunks)):
299 return 0
300 if self.reporter.invert_diff and len(final_hunks) == 0:
247 return 0301 return 0
248 patched = patches.iter_patched_from_hunks(target_lines, final_hunks)302 patched = patches.iter_patched_from_hunks(target_lines, final_hunks)
249 creator.shelve_lines(file_id, list(patched))303 creator.shelve_lines(file_id, list(patched))
304 if self.reporter.invert_diff:
305 return len(final_hunks)
250 return len(parsed.hunks) - len(final_hunks)306 return len(parsed.hunks) - len(final_hunks)
251307
252308
253309
=== modified file 'bzrlib/tests/test_shelf.py'
--- bzrlib/tests/test_shelf.py 2009-05-06 05:36:28 +0000
+++ bzrlib/tests/test_shelf.py 2009-07-14 14:35:13 +0000
@@ -40,7 +40,7 @@
4040
41class TestPrepareShelf(tests.TestCaseWithTransport):41class TestPrepareShelf(tests.TestCaseWithTransport):
4242
43 def test_shelve_rename(self):43 def prepare_shelve_rename(self):
44 tree = self.make_branch_and_tree('.')44 tree = self.make_branch_and_tree('.')
45 self.build_tree(['foo'])45 self.build_tree(['foo'])
46 tree.add(['foo'], ['foo-id'])46 tree.add(['foo'], ['foo-id'])
@@ -50,7 +50,9 @@
50 self.addCleanup(creator.finalize)50 self.addCleanup(creator.finalize)
51 self.assertEqual([('rename', 'foo-id', 'foo', 'bar')],51 self.assertEqual([('rename', 'foo-id', 'foo', 'bar')],
52 list(creator.iter_shelvable()))52 list(creator.iter_shelvable()))
53 creator.shelve_rename('foo-id')53 return creator
54
55 def check_shelve_rename(self, creator):
54 work_trans_id = creator.work_transform.trans_id_file_id('foo-id')56 work_trans_id = creator.work_transform.trans_id_file_id('foo-id')
55 self.assertEqual('foo', creator.work_transform.final_name(57 self.assertEqual('foo', creator.work_transform.final_name(
56 work_trans_id))58 work_trans_id))
@@ -58,7 +60,17 @@
58 self.assertEqual('bar', creator.shelf_transform.final_name(60 self.assertEqual('bar', creator.shelf_transform.final_name(
59 shelf_trans_id))61 shelf_trans_id))
6062
61 def test_shelve_move(self):63 def test_shelve_rename(self):
64 creator = self.prepare_shelve_rename()
65 creator.shelve_rename('foo-id')
66 self.check_shelve_rename(creator)
67
68 def test_shelve_change_handles_rename(self):
69 creator = self.prepare_shelve_rename()
70 creator.shelve_change(('rename', 'foo-id', 'foo', 'bar'))
71 self.check_shelve_rename(creator)
72
73 def prepare_shelve_move(self):
62 tree = self.make_branch_and_tree('.')74 tree = self.make_branch_and_tree('.')
63 self.build_tree(['foo/', 'bar/', 'foo/baz'])75 self.build_tree(['foo/', 'bar/', 'foo/baz'])
64 tree.add(['foo', 'bar', 'foo/baz'], ['foo-id', 'bar-id', 'baz-id'])76 tree.add(['foo', 'bar', 'foo/baz'], ['foo-id', 'bar-id', 'baz-id'])
@@ -68,7 +80,9 @@
68 self.addCleanup(creator.finalize)80 self.addCleanup(creator.finalize)
69 self.assertEqual([('rename', 'baz-id', 'foo/baz', 'bar/baz')],81 self.assertEqual([('rename', 'baz-id', 'foo/baz', 'bar/baz')],
70 list(creator.iter_shelvable()))82 list(creator.iter_shelvable()))
71 creator.shelve_rename('baz-id')83 return creator, tree
84
85 def check_shelve_move(self, creator, tree):
72 work_trans_id = creator.work_transform.trans_id_file_id('baz-id')86 work_trans_id = creator.work_transform.trans_id_file_id('baz-id')
73 work_foo = creator.work_transform.trans_id_file_id('foo-id')87 work_foo = creator.work_transform.trans_id_file_id('foo-id')
74 self.assertEqual(work_foo, creator.work_transform.final_parent(88 self.assertEqual(work_foo, creator.work_transform.final_parent(
@@ -80,6 +94,16 @@
80 creator.transform()94 creator.transform()
81 self.assertEqual('foo/baz', tree.id2path('baz-id'))95 self.assertEqual('foo/baz', tree.id2path('baz-id'))
8296
97 def test_shelve_move(self):
98 creator, tree = self.prepare_shelve_move()
99 creator.shelve_rename('baz-id')
100 self.check_shelve_move(creator, tree)
101
102 def test_shelve_change_handles_move(self):
103 creator, tree = self.prepare_shelve_move()
104 creator.shelve_change(('rename', 'baz-id', 'foo/baz', 'bar/baz'))
105 self.check_shelve_move(creator, tree)
106
83 def assertShelvedFileEqual(self, expected_content, creator, file_id):107 def assertShelvedFileEqual(self, expected_content, creator, file_id):
84 s_trans_id = creator.shelf_transform.trans_id_file_id(file_id)108 s_trans_id = creator.shelf_transform.trans_id_file_id(file_id)
85 shelf_file = creator.shelf_transform._limbo_name(s_trans_id)109 shelf_file = creator.shelf_transform._limbo_name(s_trans_id)
@@ -102,7 +126,8 @@
102 self.assertFileEqual('a\nc\n', 'foo')126 self.assertFileEqual('a\nc\n', 'foo')
103 self.assertShelvedFileEqual('b\na\n', creator, 'foo-id')127 self.assertShelvedFileEqual('b\na\n', creator, 'foo-id')
104128
105 def test_shelve_creation(self):129
130 def prepare_shelve_creation(self):
106 tree = self.make_branch_and_tree('.')131 tree = self.make_branch_and_tree('.')
107 tree.lock_write()132 tree.lock_write()
108 self.addCleanup(tree.unlock)133 self.addCleanup(tree.unlock)
@@ -114,9 +139,9 @@
114 self.assertEqual([('add file', 'bar-id', 'directory', 'bar'),139 self.assertEqual([('add file', 'bar-id', 'directory', 'bar'),
115 ('add file', 'foo-id', 'file', 'foo')],140 ('add file', 'foo-id', 'file', 'foo')],
116 sorted(list(creator.iter_shelvable())))141 sorted(list(creator.iter_shelvable())))
117 creator.shelve_creation('foo-id')142 return creator, tree
118 creator.shelve_creation('bar-id')143
119 creator.transform()144 def check_shelve_creation(self, creator, tree):
120 self.assertRaises(StopIteration,145 self.assertRaises(StopIteration,
121 tree.iter_entries_by_dir(['foo-id']).next)146 tree.iter_entries_by_dir(['foo-id']).next)
122 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')147 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
@@ -129,7 +154,22 @@
129 self.assertEqual('directory',154 self.assertEqual('directory',
130 creator.shelf_transform.final_kind(s_bar_trans_id))155 creator.shelf_transform.final_kind(s_bar_trans_id))
131156
132 def _test_shelve_symlink_creation(self, link_name, link_target):157 def test_shelve_creation(self):
158 creator, tree = self.prepare_shelve_creation()
159 creator.shelve_creation('foo-id')
160 creator.shelve_creation('bar-id')
161 creator.transform()
162 self.check_shelve_creation(creator, tree)
163
164 def test_shelve_change_handles_creation(self):
165 creator, tree = self.prepare_shelve_creation()
166 creator.shelve_change(('add file', 'foo-id', 'file', 'foo'))
167 creator.shelve_change(('add file', 'bar-id', 'directory', 'bar'))
168 creator.transform()
169 self.check_shelve_creation(creator, tree)
170
171 def _test_shelve_symlink_creation(self, link_name, link_target,
172 shelve_change=False):
133 self.requireFeature(tests.SymlinkFeature)173 self.requireFeature(tests.SymlinkFeature)
134 tree = self.make_branch_and_tree('.')174 tree = self.make_branch_and_tree('.')
135 tree.lock_write()175 tree.lock_write()
@@ -141,7 +181,10 @@
141 self.addCleanup(creator.finalize)181 self.addCleanup(creator.finalize)
142 self.assertEqual([('add file', 'foo-id', 'symlink', link_name)],182 self.assertEqual([('add file', 'foo-id', 'symlink', link_name)],
143 list(creator.iter_shelvable()))183 list(creator.iter_shelvable()))
144 creator.shelve_creation('foo-id')184 if shelve_change:
185 creator.shelve_change(('add file', 'foo-id', 'symlink', link_name))
186 else:
187 creator.shelve_creation('foo-id')
145 creator.transform()188 creator.transform()
146 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')189 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
147 self.failIfExists(link_name)190 self.failIfExists(link_name)
@@ -158,8 +201,12 @@
158 self._test_shelve_symlink_creation(u'fo\N{Euro Sign}o',201 self._test_shelve_symlink_creation(u'fo\N{Euro Sign}o',
159 u'b\N{Euro Sign}ar')202 u'b\N{Euro Sign}ar')
160203
204 def test_shelve_change_handles_symlink_creation(self):
205 self._test_shelve_symlink_creation('foo', 'bar', shelve_change=True)
206
161 def _test_shelve_symlink_target_change(self, link_name,207 def _test_shelve_symlink_target_change(self, link_name,
162 old_target, new_target):208 old_target, new_target,
209 shelve_change=False):
163 self.requireFeature(tests.SymlinkFeature)210 self.requireFeature(tests.SymlinkFeature)
164 tree = self.make_branch_and_tree('.')211 tree = self.make_branch_and_tree('.')
165 tree.lock_write()212 tree.lock_write()
@@ -174,7 +221,11 @@
174 self.assertEqual([('modify target', 'foo-id', link_name,221 self.assertEqual([('modify target', 'foo-id', link_name,
175 old_target, new_target)],222 old_target, new_target)],
176 list(creator.iter_shelvable()))223 list(creator.iter_shelvable()))
177 creator.shelve_modify_target('foo-id')224 if shelve_change:
225 creator.shelve_change(('modify target', 'foo-id', link_name,
226 old_target, new_target))
227 else:
228 creator.shelve_modify_target('foo-id')
178 creator.transform()229 creator.transform()
179 self.assertEqual(old_target, osutils.readlink(link_name))230 self.assertEqual(old_target, osutils.readlink(link_name))
180 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')231 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
@@ -191,6 +242,10 @@
191 self._test_shelve_symlink_target_change(242 self._test_shelve_symlink_target_change(
192 u'fo\N{Euro Sign}o', u'b\N{Euro Sign}ar', u'b\N{Euro Sign}az')243 u'fo\N{Euro Sign}o', u'b\N{Euro Sign}ar', u'b\N{Euro Sign}az')
193244
245 def test_shelve_change_handles_symlink_target_change(self):
246 self._test_shelve_symlink_target_change('foo', 'bar', 'baz',
247 shelve_change=True)
248
194 def test_shelve_creation_no_contents(self):249 def test_shelve_creation_no_contents(self):
195 tree = self.make_branch_and_tree('.')250 tree = self.make_branch_and_tree('.')
196 tree.lock_write()251 tree.lock_write()
@@ -213,7 +268,7 @@
213 creator.shelf_transform.final_file_id(s_trans_id))268 creator.shelf_transform.final_file_id(s_trans_id))
214 self.failIfExists('foo')269 self.failIfExists('foo')
215270
216 def test_shelve_deletion(self):271 def prepare_shelve_deletion(self):
217 tree = self.make_branch_and_tree('tree')272 tree = self.make_branch_and_tree('tree')
218 tree.lock_write()273 tree.lock_write()
219 self.addCleanup(tree.unlock)274 self.addCleanup(tree.unlock)
@@ -228,13 +283,27 @@
228 self.assertEqual([('delete file', 'bar-id', 'file', 'foo/bar'),283 self.assertEqual([('delete file', 'bar-id', 'file', 'foo/bar'),
229 ('delete file', 'foo-id', 'directory', 'foo')],284 ('delete file', 'foo-id', 'directory', 'foo')],
230 sorted(list(creator.iter_shelvable())))285 sorted(list(creator.iter_shelvable())))
231 creator.shelve_deletion('foo-id')286 return creator, tree
232 creator.shelve_deletion('bar-id')287
233 creator.transform()288 def check_shelve_deletion(self, tree):
234 self.assertTrue('foo-id' in tree)289 self.assertTrue('foo-id' in tree)
235 self.assertTrue('bar-id' in tree)290 self.assertTrue('bar-id' in tree)
236 self.assertFileEqual('baz', 'tree/foo/bar')291 self.assertFileEqual('baz', 'tree/foo/bar')
237292
293 def test_shelve_deletion(self):
294 creator, tree = self.prepare_shelve_deletion()
295 creator.shelve_deletion('foo-id')
296 creator.shelve_deletion('bar-id')
297 creator.transform()
298 self.check_shelve_deletion(tree)
299
300 def test_shelve_change_handles_deletion(self):
301 creator, tree = self.prepare_shelve_deletion()
302 creator.shelve_change(('delete file', 'foo-id', 'directory', 'foo'))
303 creator.shelve_change(('delete file', 'bar-id', 'file', 'foo/bar'))
304 creator.transform()
305 self.check_shelve_deletion(tree)
306
238 def test_shelve_delete_contents(self):307 def test_shelve_delete_contents(self):
239 tree = self.make_branch_and_tree('tree')308 tree = self.make_branch_and_tree('tree')
240 self.build_tree(['tree/foo',])309 self.build_tree(['tree/foo',])
@@ -249,7 +318,7 @@
249 creator.transform()318 creator.transform()
250 self.failUnlessExists('tree/foo')319 self.failUnlessExists('tree/foo')
251320
252 def test_shelve_change_kind(self):321 def prepare_shelve_change_kind(self):
253 tree = self.make_branch_and_tree('tree')322 tree = self.make_branch_and_tree('tree')
254 self.build_tree_contents([('tree/foo', 'bar')])323 self.build_tree_contents([('tree/foo', 'bar')])
255 tree.add('foo', 'foo-id')324 tree.add('foo', 'foo-id')
@@ -260,12 +329,33 @@
260 self.addCleanup(creator.finalize)329 self.addCleanup(creator.finalize)
261 self.assertEqual([('change kind', 'foo-id', 'file', 'directory',330 self.assertEqual([('change kind', 'foo-id', 'file', 'directory',
262 'foo')], sorted(list(creator.iter_shelvable())))331 'foo')], sorted(list(creator.iter_shelvable())))
332 return creator
333
334 def check_shelve_change_kind(self, creator):
335 self.assertFileEqual('bar', 'tree/foo')
336 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')
337 self.assertEqual('directory',
338 creator.shelf_transform._new_contents[s_trans_id])
339
340 def test_shelve_change_kind(self):
341 creator = self.prepare_shelve_change_kind()
263 creator.shelve_content_change('foo-id')342 creator.shelve_content_change('foo-id')
264 creator.transform()343 creator.transform()
265 self.assertFileEqual('bar', 'tree/foo')344 self.check_shelve_change_kind(creator)
266 s_trans_id = creator.shelf_transform.trans_id_file_id('foo-id')345
267 self.assertEqual('directory',346 def test_shelve_change_handles_change_kind(self):
268 creator.shelf_transform._new_contents[s_trans_id])347 creator = self.prepare_shelve_change_kind()
348 creator.shelve_change(('change kind', 'foo-id', 'file', 'directory',
349 'foo'))
350 creator.transform()
351 self.check_shelve_change_kind(creator)
352
353 def test_shelve_change_unknown_change(self):
354 tree = self.make_branch_and_tree('tree')
355 creator = shelf.ShelfCreator(tree, tree.basis_tree())
356 self.addCleanup(creator.finalize)
357 e = self.assertRaises(ValueError, creator.shelve_change, ('unknown',))
358 self.assertEqual('Unknown change kind: "unknown"', str(e))
269359
270 def test_shelve_unversion(self):360 def test_shelve_unversion(self):
271 tree = self.make_branch_and_tree('tree')361 tree = self.make_branch_and_tree('tree')
272362
=== modified file 'bzrlib/tests/test_shelf_ui.py'
--- bzrlib/tests/test_shelf_ui.py 2009-03-23 14:59:43 +0000
+++ bzrlib/tests/test_shelf_ui.py 2009-07-14 14:35:13 +0000
@@ -27,10 +27,10 @@
2727
28 def __init__(self, work_tree, target_tree, diff_writer=None,28 def __init__(self, work_tree, target_tree, diff_writer=None,
29 auto=False, auto_apply=False, file_list=None, message=None,29 auto=False, auto_apply=False, file_list=None, message=None,
30 destroy=False):30 destroy=False, reporter=None):
31 shelf_ui.Shelver.__init__(self, work_tree, target_tree, diff_writer,31 shelf_ui.Shelver.__init__(self, work_tree, target_tree, diff_writer,
32 auto, auto_apply, file_list, message,32 auto, auto_apply, file_list, message,
33 destroy)33 destroy, reporter=reporter)
34 self.expected = []34 self.expected = []
35 self.diff_writer = StringIO()35 self.diff_writer = StringIO()
3636
@@ -165,6 +165,7 @@
165 shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')165 shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
166166
167 def test_shelve_modify_target(self):167 def test_shelve_modify_target(self):
168 self.requireFeature(tests.SymlinkFeature)
168 tree = self.create_shelvable_tree()169 tree = self.create_shelvable_tree()
169 os.symlink('bar', 'tree/baz')170 os.symlink('bar', 'tree/baz')
170 tree.add('baz', 'baz-id')171 tree.add('baz', 'baz-id')
@@ -224,6 +225,108 @@
224 self.assertFileEqual(LINES_AJ, 'tree/foo')225 self.assertFileEqual(LINES_AJ, 'tree/foo')
225226
226227
228class TestApplyReporter(TestShelver):
229
230 def test_shelve_not_diff(self):
231 tree = self.create_shelvable_tree()
232 shelver = ExpectShelver(tree, tree.basis_tree(),
233 reporter=shelf_ui.ApplyReporter())
234 shelver.expect('Apply change? [yNfq?]', 'n')
235 shelver.expect('Apply change? [yNfq?]', 'n')
236 # No final shelving prompt because no changes were selected
237 shelver.run()
238 self.assertFileEqual(LINES_ZY, 'tree/foo')
239
240 def test_shelve_diff_no(self):
241 tree = self.create_shelvable_tree()
242 shelver = ExpectShelver(tree, tree.basis_tree(),
243 reporter=shelf_ui.ApplyReporter())
244 shelver.expect('Apply change? [yNfq?]', 'y')
245 shelver.expect('Apply change? [yNfq?]', 'y')
246 shelver.expect('Apply 2 change(s)? [yNfq?]', 'n')
247 shelver.run()
248 self.assertFileEqual(LINES_ZY, 'tree/foo')
249
250 def test_shelve_diff(self):
251 tree = self.create_shelvable_tree()
252 shelver = ExpectShelver(tree, tree.basis_tree(),
253 reporter=shelf_ui.ApplyReporter())
254 shelver.expect('Apply change? [yNfq?]', 'y')
255 shelver.expect('Apply change? [yNfq?]', 'y')
256 shelver.expect('Apply 2 change(s)? [yNfq?]', 'y')
257 shelver.run()
258 self.assertFileEqual(LINES_AJ, 'tree/foo')
259
260 def test_shelve_binary_change(self):
261 tree = self.create_shelvable_tree()
262 self.build_tree_contents([('tree/foo', '\x00')])
263 shelver = ExpectShelver(tree, tree.basis_tree(),
264 reporter=shelf_ui.ApplyReporter())
265 shelver.expect('Apply binary changes? [yNfq?]', 'y')
266 shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
267 shelver.run()
268 self.assertFileEqual(LINES_AJ, 'tree/foo')
269
270 def test_shelve_rename(self):
271 tree = self.create_shelvable_tree()
272 tree.rename_one('foo', 'bar')
273 shelver = ExpectShelver(tree, tree.basis_tree(),
274 reporter=shelf_ui.ApplyReporter())
275 shelver.expect('Rename "bar" => "foo"? [yNfq?]', 'y')
276 shelver.expect('Apply change? [yNfq?]', 'y')
277 shelver.expect('Apply change? [yNfq?]', 'y')
278 shelver.expect('Apply 3 change(s)? [yNfq?]', 'y')
279 shelver.run()
280 self.assertFileEqual(LINES_AJ, 'tree/foo')
281
282 def test_shelve_deletion(self):
283 tree = self.create_shelvable_tree()
284 os.unlink('tree/foo')
285 shelver = ExpectShelver(tree, tree.basis_tree(),
286 reporter=shelf_ui.ApplyReporter())
287 shelver.expect('Add file "foo"? [yNfq?]', 'y')
288 shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
289 shelver.run()
290 self.assertFileEqual(LINES_AJ, 'tree/foo')
291
292 def test_shelve_creation(self):
293 tree = self.make_branch_and_tree('tree')
294 tree.commit('add tree root')
295 self.build_tree(['tree/foo'])
296 tree.add('foo')
297 shelver = ExpectShelver(tree, tree.basis_tree(),
298 reporter=shelf_ui.ApplyReporter())
299 shelver.expect('Delete file "foo"? [yNfq?]', 'y')
300 shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
301 shelver.run()
302 self.failIfExists('tree/foo')
303
304 def test_shelve_kind_change(self):
305 tree = self.create_shelvable_tree()
306 os.unlink('tree/foo')
307 os.mkdir('tree/foo')
308 shelver = ExpectShelver(tree, tree.basis_tree(),
309 reporter=shelf_ui.ApplyReporter())
310 shelver.expect('Change "foo" from directory to a file? [yNfq?]', 'y')
311 shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
312
313 def test_shelve_modify_target(self):
314 self.requireFeature(tests.SymlinkFeature)
315 tree = self.create_shelvable_tree()
316 os.symlink('bar', 'tree/baz')
317 tree.add('baz', 'baz-id')
318 tree.commit("Add symlink")
319 os.unlink('tree/baz')
320 os.symlink('vax', 'tree/baz')
321 shelver = ExpectShelver(tree, tree.basis_tree(),
322 reporter=shelf_ui.ApplyReporter())
323 shelver.expect('Change target of "baz" from "vax" to "bar"? [yNfq?]',
324 'y')
325 shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
326 shelver.run()
327 self.assertEqual('bar', os.readlink('tree/baz'))
328
329
227class TestUnshelver(tests.TestCaseWithTransport):330class TestUnshelver(tests.TestCaseWithTransport):
228331
229 def create_tree_with_shelf(self):332 def create_tree_with_shelf(self):