Merge lp:~vila/bzr/856261-unshelve-line-based into lp:bzr

Proposed by Vincent Ladeuil
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 6175
Proposed branch: lp:~vila/bzr/856261-unshelve-line-based
Merge into: lp:bzr
Diff against target: 182 lines (+97/-7)
4 files modified
bzrlib/shelf_ui.py (+26/-4)
bzrlib/tests/script.py (+8/-0)
bzrlib/tests/test_script.py (+56/-3)
doc/en/release-notes/bzr-2.5.txt (+7/-0)
To merge this branch: bzr merge lp:~vila/bzr/856261-unshelve-line-based
Reviewer Review Type Date Requested Status
Martin Packman (community) Needs Fixing
Vincent Ladeuil Needs Information
Review via email: mp+77195@code.launchpad.net

Commit message

Allows bzr shelve to be used in test scripts and under emacs shells

Description of the change

I've been deprived from using shelve under emacs for years.

But when I ran into bug #856261 trying to reproduce bug #850594, I thought
it may be worth to have another go at it.

The fix is simple: since osutils.getchar() requires a true term (which
neither emacs not the test scripts provide), I handle the input as a
line-based one which is exactly what emacs does and also exactly what the
test script interface provides :)

I'd like feedback on how we want to configure this feature, knowing that an
environment variable may be more appropriate here (not everybody use
terminals under emacs only and the test script need a way to force the
behavior). Or should I just consider that the fix for #491196 is around the
corner and name the config variable... shelve.line_based_input ?
shelve.line_ui ? both boolean defaulting to False or shelve.input
(defaulting to 'char' accepting 'line' ?)

To post a comment you must log in.
Revision history for this message
Benoit Pierre (benoit.pierre) wrote :

Excerpts from Vincent Ladeuil's message of Tue Sep 27 16:37:19 UTC 2011:
> Vincent Ladeuil has proposed merging lp:~vila/bzr/856261-unshelve-line-based into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
> For more details, see:
> https://code.launchpad.net/~vila/bzr/856261-unshelve-line-based/+merge/77195
>
> I've been deprived from using shelve under emacs for years.
>
> But when I ran into bug #856261 trying to reproduce bug #850594, I thought
> it may be worth to have another go at it.
>
> The fix is simple: since osutils.getchar() requires a true term (which
> neither emacs not the test scripts provide), I handle the input as a
> line-based one which is exactly what emacs does and also exactly what the
> test script interface provides :)
>
> I'd like feedback on how we want to configure this feature, knowing that an
> environment variable may be more appropriate here (not everybody use
> terminals under emacs only and the test script need a way to force the
> behavior). Or should I just consider that the fix for #491196 is around the
> corner and name the config variable... shelve.line_based_input ?
> shelve.line_ui ? both boolean defaulting to False or shelve.input
> (defaulting to 'char' accepting 'line' ?)

I also ran into problems using shelve with my VIM plugin. Other commands
use ui.get_boolean, but there is no corresponding method for handling
more complex choices. So even after providing my own UI class
implementation, I cannot use shelve.

In VIM there is a confirm function:

confirm({msg} [, {choices} [, {default} [, {type}]]])

Which can be used like this:

confirm("Save changes?", "&Yes\n&No\n&Cancel")

Where the letter after the '&' is the shortcut key for that choice.

So I have been working on a branch to implement something just like
this (see attached patch).

N.B.: your patch removes a call to gettext.
--
A: Because it destroys the flow of conversation.
Q: Why is top posting dumb?

1# Bazaar merge directive format 2 (Bazaar 0.90)
2# revision_id: benoit.pierre@gmail.com-20110927192241-jfqqjzm2e6ojjfq0
3# target_branch: lp:bzr
4# testament_sha1: b3590b8caa798f8691f0e18ee303b9dbd983fd09
5# timestamp: 2011-09-27 21:23:43 +0200
6# base_revision_id: pqm@pqm.ubuntu.com-20110610161957-hh5ni839m7r3wsan
7#
8# Begin patch
9=== modified file 'bzrlib/shelf_ui.py'
10--- bzrlib/shelf_ui.py 2010-11-04 17:48:47 +0000
11+++ bzrlib/shelf_ui.py 2011-09-27 19:22:41 +0000
12@@ -251,23 +251,8 @@
13 diff_file.seek(0)
14 return patches.parse_patch(diff_file)
15
16- def prompt(self, message):
17- """Prompt the user for a character.
18-
19- :param message: The message to prompt a user with.
20- :return: A character.
21- """
22- if not sys.stdin.isatty():
23- # Since there is no controlling terminal we will hang when trying
24- # to prompt the user, better abort now. See
25- # https://code.launchpad.net/~bialix/bzr/shelve-no-tty/+merge/14905
26- # for more context.
27- raise errors.BzrError("You need a controlling terminal.")
28- sys.stdout.write(message)
29- char = osutils.getchar()
30- sys.stdout.write("\r" + ' ' * len(message) + '\r')
31- sys.stdout.flush()
32- return char
33+ def prompt(self, message, choices, default):
34+ return ui.ui_factory.confirm(message, choices, default=default)
35
36 def prompt_bool(self, question, long=False, allow_editor=False):
37 """Prompt the user with a yes/no question.
38@@ -279,16 +264,11 @@
39 """
40 if self.auto:
41 return True
42- editor_string = ''
43- if long:
44- if allow_editor:
45- editor_string = '(E)dit manually, '
46- prompt = ' [(y)es, (N)o, %s(f)inish, or (q)uit]' % editor_string
47- else:
48- if allow_editor:
49- editor_string = 'e'
50- prompt = ' [yN%sfq?]' % editor_string
51- char = self.prompt(question + prompt)
52+ choices = '&yes\n&No'
53+ if allow_editor:
54+ choices += '\n&edit manually'
55+ choices += '\n&finish\n&quit'
56+ char = self.prompt(question, choices, 'n')
57 if char == 'y':
58 return True
59 elif char == 'e' and allow_editor:
60
61=== modified file 'bzrlib/tests/blackbox/test_break_lock.py'
62--- bzrlib/tests/blackbox/test_break_lock.py 2011-03-10 13:29:54 +0000
63+++ bzrlib/tests/blackbox/test_break_lock.py 2011-09-27 19:22:41 +0000
64@@ -91,7 +91,7 @@
65 self.master_branch.lock_write()
66 # run the break-lock
67 # we need 5 yes's - wt, branch, repo, bound branch, bound repo.
68- self.run_bzr('break-lock checkout', stdin="y\ny\ny\ny\n")
69+ self.run_bzr('break-lock checkout', stdin="yyyy")
70 # a new tree instance should be lockable
71 br = branch.Branch.open('checkout')
72 br.lock_write()
73
74=== modified file 'bzrlib/tests/blackbox/test_uncommit.py'
75--- bzrlib/tests/blackbox/test_uncommit.py 2010-09-15 09:35:42 +0000
76+++ bzrlib/tests/blackbox/test_uncommit.py 2011-09-27 19:22:41 +0000
77@@ -72,7 +72,7 @@
78 $ bzr uncommit
79 ...
80 The above revision(s) will be removed.
81- 2>Uncommit these revisions? [y/n]:
82+ 2>Uncommit these revisions? ([y]es, [n]o): n
83 <n
84 Canceled
85 """)
86
87=== modified file 'bzrlib/tests/test_script.py'
88--- bzrlib/tests/test_script.py 2011-05-16 13:39:39 +0000
89+++ bzrlib/tests/test_script.py 2011-09-27 19:22:41 +0000
90@@ -580,12 +580,12 @@
91 self.addCleanup(commands.builtin_command_registry.remove, 'test-confirm')
92 self.run_script("""
93 $ bzr test-confirm
94- 2>Really do it? [y/n]:
95- <yes
96+ 2>Really do it? ([y]es, [n]o): y
97+ <y
98 Do it!
99 $ bzr test-confirm
100- 2>Really do it? [y/n]:
101- <no
102+ 2>Really do it? ([y]es, [n]o): n
103+ <n
104 ok, no
105 """)
106
107
108=== modified file 'bzrlib/tests/test_shelf_ui.py'
109--- bzrlib/tests/test_shelf_ui.py 2011-06-09 14:40:22 +0000
110+++ bzrlib/tests/test_shelf_ui.py 2011-09-27 19:22:41 +0000
111@@ -41,16 +41,18 @@
112 self.expected = []
113 self.diff_writer = StringIO()
114
115- def expect(self, prompt, response):
116- self.expected.append((prompt, response))
117+ def expect(self, message, choices, response):
118+ self.expected.append((message, choices, response))
119
120- def prompt(self, message):
121+ def prompt(self, message, choices, default):
122 try:
123- prompt, response = self.expected.pop(0)
124+ expected_message, expected_choices, response = self.expected.pop(0)
125 except IndexError:
126 raise AssertionError('Unexpected prompt: %s' % message)
127- if prompt != message:
128+ if message != expected_message:
129 raise AssertionError('Wrong prompt: %s' % message)
130+ if choices != expected_choices:
131+ raise AssertionError('Wrong choices: %s' % choices)
132 return response
133
134
135@@ -83,7 +85,7 @@
136 shelver = ExpectShelver(tree, tree.basis_tree())
137 self.addCleanup(shelver.finalize)
138 e = self.assertRaises(AssertionError, shelver.run)
139- self.assertEqual('Unexpected prompt: Shelve? [yNfq?]', str(e))
140+ self.assertEqual('Unexpected prompt: Shelve?', str(e))
141
142 def test_wrong_prompt_failure(self):
143 tree = self.create_shelvable_tree()
144@@ -91,9 +93,9 @@
145 self.addCleanup(tree.unlock)
146 shelver = ExpectShelver(tree, tree.basis_tree())
147 self.addCleanup(shelver.finalize)
148- shelver.expect('foo', 'y')
149+ shelver.expect('foo', 'bar', 'y')
150 e = self.assertRaises(AssertionError, shelver.run)
151- self.assertEqual('Wrong prompt: Shelve? [yNfq?]', str(e))
152+ self.assertEqual('Wrong prompt: Shelve?', str(e))
153
154 def test_shelve_not_diff(self):
155 tree = self.create_shelvable_tree()
156@@ -101,8 +103,8 @@
157 self.addCleanup(tree.unlock)
158 shelver = ExpectShelver(tree, tree.basis_tree())
159 self.addCleanup(shelver.finalize)
160- shelver.expect('Shelve? [yNfq?]', 'n')
161- shelver.expect('Shelve? [yNfq?]', 'n')
162+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'n')
163+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'n')
164 # No final shelving prompt because no changes were selected
165 shelver.run()
166 self.assertFileEqual(LINES_ZY, 'tree/foo')
167@@ -113,9 +115,9 @@
168 self.addCleanup(tree.unlock)
169 shelver = ExpectShelver(tree, tree.basis_tree())
170 self.addCleanup(shelver.finalize)
171- shelver.expect('Shelve? [yNfq?]', 'y')
172- shelver.expect('Shelve? [yNfq?]', 'y')
173- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'n')
174+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'y')
175+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'y')
176+ shelver.expect('Shelve 2 change(s)?', '&yes\n&No\n&finish\n&quit', 'n')
177 shelver.run()
178 self.assertFileEqual(LINES_ZY, 'tree/foo')
179
180@@ -125,9 +127,9 @@
181 self.addCleanup(tree.unlock)
182 shelver = ExpectShelver(tree, tree.basis_tree())
183 self.addCleanup(shelver.finalize)
184- shelver.expect('Shelve? [yNfq?]', 'y')
185- shelver.expect('Shelve? [yNfq?]', 'y')
186- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'y')
187+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'y')
188+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'y')
189+ shelver.expect('Shelve 2 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
190 shelver.run()
191 self.assertFileEqual(LINES_AJ, 'tree/foo')
192
193@@ -137,9 +139,9 @@
194 self.addCleanup(tree.unlock)
195 shelver = ExpectShelver(tree, tree.basis_tree())
196 self.addCleanup(shelver.finalize)
197- shelver.expect('Shelve? [yNfq?]', 'y')
198- shelver.expect('Shelve? [yNfq?]', 'n')
199- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
200+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'y')
201+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'n')
202+ shelver.expect('Shelve 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
203 shelver.run()
204 self.assertFileEqual(LINES_AY, 'tree/foo')
205
206@@ -150,8 +152,8 @@
207 self.addCleanup(tree.unlock)
208 shelver = ExpectShelver(tree, tree.basis_tree())
209 self.addCleanup(shelver.finalize)
210- shelver.expect('Shelve binary changes? [yNfq?]', 'y')
211- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
212+ shelver.expect('Shelve binary changes?', '&yes\n&No\n&finish\n&quit', 'y')
213+ shelver.expect('Shelve 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
214 shelver.run()
215 self.assertFileEqual(LINES_AJ, 'tree/foo')
216
217@@ -162,10 +164,10 @@
218 self.addCleanup(tree.unlock)
219 shelver = ExpectShelver(tree, tree.basis_tree())
220 self.addCleanup(shelver.finalize)
221- shelver.expect('Shelve renaming "foo" => "bar"? [yNfq?]', 'y')
222- shelver.expect('Shelve? [yNfq?]', 'y')
223- shelver.expect('Shelve? [yNfq?]', 'y')
224- shelver.expect('Shelve 3 change(s)? [yNfq?]', 'y')
225+ shelver.expect('Shelve renaming "foo" => "bar"?', '&yes\n&No\n&finish\n&quit', 'y')
226+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'y')
227+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'y')
228+ shelver.expect('Shelve 3 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
229 shelver.run()
230 self.assertFileEqual(LINES_AJ, 'tree/foo')
231
232@@ -176,8 +178,8 @@
233 self.addCleanup(tree.unlock)
234 shelver = ExpectShelver(tree, tree.basis_tree())
235 self.addCleanup(shelver.finalize)
236- shelver.expect('Shelve removing file "foo"? [yNfq?]', 'y')
237- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
238+ shelver.expect('Shelve removing file "foo"?', '&yes\n&No\n&finish\n&quit', 'y')
239+ shelver.expect('Shelve 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
240 shelver.run()
241 self.assertFileEqual(LINES_AJ, 'tree/foo')
242
243@@ -190,8 +192,8 @@
244 self.addCleanup(tree.unlock)
245 shelver = ExpectShelver(tree, tree.basis_tree())
246 self.addCleanup(shelver.finalize)
247- shelver.expect('Shelve adding file "foo"? [yNfq?]', 'y')
248- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
249+ shelver.expect('Shelve adding file "foo"?', '&yes\n&No\n&finish\n&quit', 'y')
250+ shelver.expect('Shelve 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
251 shelver.run()
252 self.assertPathDoesNotExist('tree/foo')
253
254@@ -203,9 +205,9 @@
255 self.addCleanup(tree.unlock)
256 shelver = ExpectShelver(tree, tree.basis_tree())
257 self.addCleanup(shelver.finalize)
258- shelver.expect('Shelve changing "foo" from file to directory? [yNfq?]',
259+ shelver.expect('Shelve changing "foo" from file to directory?', '&yes\n&No\n&finish\n&quit',
260 'y')
261- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
262+ shelver.expect('Shelve 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
263
264 def test_shelve_modify_target(self):
265 self.requireFeature(tests.SymlinkFeature)
266@@ -220,8 +222,8 @@
267 shelver = ExpectShelver(tree, tree.basis_tree())
268 self.addCleanup(shelver.finalize)
269 shelver.expect('Shelve changing target of "baz" from "bar" to '
270- '"vax"? [yNfq?]', 'y')
271- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
272+ '"vax"?', '&yes\n&No\n&finish\n&quit', 'y')
273+ shelver.expect('Shelve 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
274 shelver.run()
275 self.assertEqual('bar', os.readlink('tree/baz'))
276
277@@ -231,8 +233,8 @@
278 self.addCleanup(tree.unlock)
279 shelver = ExpectShelver(tree, tree.basis_tree())
280 self.addCleanup(shelver.finalize)
281- shelver.expect('Shelve? [yNfq?]', 'f')
282- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'y')
283+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'f')
284+ shelver.expect('Shelve 2 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
285 shelver.run()
286 self.assertFileEqual(LINES_AJ, 'tree/foo')
287
288@@ -242,7 +244,7 @@
289 self.addCleanup(tree.unlock)
290 shelver = ExpectShelver(tree, tree.basis_tree())
291 self.addCleanup(shelver.finalize)
292- shelver.expect('Shelve? [yNfq?]', 'q')
293+ shelver.expect('Shelve?', '&yes\n&No\n&finish\n&quit', 'q')
294 self.assertRaises(errors.UserAbort, shelver.run)
295 self.assertFileEqual(LINES_ZY, 'tree/foo')
296
297@@ -264,19 +266,8 @@
298 self.addCleanup(tree.unlock)
299 shelver = ExpectShelver(tree, tree.basis_tree(), file_list=['bar'])
300 self.addCleanup(shelver.finalize)
301- shelver.expect('Shelve adding file "bar"? [yNfq?]', 'y')
302- shelver.expect('Shelve 1 change(s)? [yNfq?]', 'y')
303- shelver.run()
304-
305- def test_shelve_help(self):
306- tree = self.create_shelvable_tree()
307- tree.lock_tree_write()
308- self.addCleanup(tree.unlock)
309- shelver = ExpectShelver(tree, tree.basis_tree())
310- self.addCleanup(shelver.finalize)
311- shelver.expect('Shelve? [yNfq?]', '?')
312- shelver.expect('Shelve? [(y)es, (N)o, (f)inish, or (q)uit]', 'f')
313- shelver.expect('Shelve 2 change(s)? [yNfq?]', 'y')
314+ shelver.expect('Shelve adding file "bar"?', '&yes\n&No\n&finish\n&quit', 'y')
315+ shelver.expect('Shelve 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
316 shelver.run()
317
318 def test_shelve_destroy(self):
319@@ -339,8 +330,8 @@
320 shelver = ExpectShelver(tree, tree.basis_tree(),
321 reporter=shelf_ui.ApplyReporter())
322 self.addCleanup(shelver.finalize)
323- shelver.expect('Apply change? [yNfq?]', 'n')
324- shelver.expect('Apply change? [yNfq?]', 'n')
325+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'n')
326+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'n')
327 # No final shelving prompt because no changes were selected
328 shelver.run()
329 self.assertFileEqual(LINES_ZY, 'tree/foo')
330@@ -352,9 +343,9 @@
331 shelver = ExpectShelver(tree, tree.basis_tree(),
332 reporter=shelf_ui.ApplyReporter())
333 self.addCleanup(shelver.finalize)
334- shelver.expect('Apply change? [yNfq?]', 'y')
335- shelver.expect('Apply change? [yNfq?]', 'y')
336- shelver.expect('Apply 2 change(s)? [yNfq?]', 'n')
337+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'y')
338+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'y')
339+ shelver.expect('Apply 2 change(s)?', '&yes\n&No\n&finish\n&quit', 'n')
340 shelver.run()
341 self.assertFileEqual(LINES_ZY, 'tree/foo')
342
343@@ -365,9 +356,9 @@
344 shelver = ExpectShelver(tree, tree.basis_tree(),
345 reporter=shelf_ui.ApplyReporter())
346 self.addCleanup(shelver.finalize)
347- shelver.expect('Apply change? [yNfq?]', 'y')
348- shelver.expect('Apply change? [yNfq?]', 'y')
349- shelver.expect('Apply 2 change(s)? [yNfq?]', 'y')
350+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'y')
351+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'y')
352+ shelver.expect('Apply 2 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
353 shelver.run()
354 self.assertFileEqual(LINES_AJ, 'tree/foo')
355
356@@ -379,8 +370,8 @@
357 shelver = ExpectShelver(tree, tree.basis_tree(),
358 reporter=shelf_ui.ApplyReporter())
359 self.addCleanup(shelver.finalize)
360- shelver.expect('Apply binary changes? [yNfq?]', 'y')
361- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
362+ shelver.expect('Apply binary changes?', '&yes\n&No\n&finish\n&quit', 'y')
363+ shelver.expect('Apply 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
364 shelver.run()
365 self.assertFileEqual(LINES_AJ, 'tree/foo')
366
367@@ -392,10 +383,10 @@
368 shelver = ExpectShelver(tree, tree.basis_tree(),
369 reporter=shelf_ui.ApplyReporter())
370 self.addCleanup(shelver.finalize)
371- shelver.expect('Rename "bar" => "foo"? [yNfq?]', 'y')
372- shelver.expect('Apply change? [yNfq?]', 'y')
373- shelver.expect('Apply change? [yNfq?]', 'y')
374- shelver.expect('Apply 3 change(s)? [yNfq?]', 'y')
375+ shelver.expect('Rename "bar" => "foo"?', '&yes\n&No\n&finish\n&quit', 'y')
376+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'y')
377+ shelver.expect('Apply change?', '&yes\n&No\n&finish\n&quit', 'y')
378+ shelver.expect('Apply 3 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
379 shelver.run()
380 self.assertFileEqual(LINES_AJ, 'tree/foo')
381
382@@ -407,8 +398,8 @@
383 shelver = ExpectShelver(tree, tree.basis_tree(),
384 reporter=shelf_ui.ApplyReporter())
385 self.addCleanup(shelver.finalize)
386- shelver.expect('Add file "foo"? [yNfq?]', 'y')
387- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
388+ shelver.expect('Add file "foo"?', '&yes\n&No\n&finish\n&quit', 'y')
389+ shelver.expect('Apply 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
390 shelver.run()
391 self.assertFileEqual(LINES_AJ, 'tree/foo')
392
393@@ -422,8 +413,8 @@
394 shelver = ExpectShelver(tree, tree.basis_tree(),
395 reporter=shelf_ui.ApplyReporter())
396 self.addCleanup(shelver.finalize)
397- shelver.expect('Delete file "foo"? [yNfq?]', 'y')
398- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
399+ shelver.expect('Delete file "foo"?', '&yes\n&No\n&finish\n&quit', 'y')
400+ shelver.expect('Apply 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
401 shelver.run()
402 self.assertPathDoesNotExist('tree/foo')
403
404@@ -436,8 +427,8 @@
405 shelver = ExpectShelver(tree, tree.basis_tree(),
406 reporter=shelf_ui.ApplyReporter())
407 self.addCleanup(shelver.finalize)
408- shelver.expect('Change "foo" from directory to a file? [yNfq?]', 'y')
409- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
410+ shelver.expect('Change "foo" from directory to a file?', '&yes\n&No\n&finish\n&quit', 'y')
411+ shelver.expect('Apply 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
412
413 def test_shelve_modify_target(self):
414 self.requireFeature(tests.SymlinkFeature)
415@@ -452,9 +443,9 @@
416 shelver = ExpectShelver(tree, tree.basis_tree(),
417 reporter=shelf_ui.ApplyReporter())
418 self.addCleanup(shelver.finalize)
419- shelver.expect('Change target of "baz" from "vax" to "bar"? [yNfq?]',
420+ shelver.expect('Change target of "baz" from "vax" to "bar"?', '&yes\n&No\n&finish\n&quit',
421 'y')
422- shelver.expect('Apply 1 change(s)? [yNfq?]', 'y')
423+ shelver.expect('Apply 1 change(s)?', '&yes\n&No\n&finish\n&quit', 'y')
424 shelver.run()
425 self.assertEqual('bar', os.readlink('tree/baz'))
426
427
428=== modified file 'bzrlib/tests/test_ui.py'
429--- bzrlib/tests/test_ui.py 2011-05-16 13:39:39 +0000
430+++ bzrlib/tests/test_ui.py 2011-09-27 19:22:41 +0000
431@@ -109,13 +109,14 @@
432 pb.finished()
433
434 def test_text_ui_get_boolean(self):
435- stdin = tests.StringIOWrapper("y\n" # True
436- "n\n" # False
437- "yes with garbage\nY\n" # True
438- "not an answer\nno\n" # False
439- "I'm sure!\nyes\n" # True
440- "NO\n" # False
441- "foo\n")
442+ stdin = tests.StringIOWrapper("y" # True
443+ "n" # False
444+ "Y" # True
445+ "N" # False
446+ "\r\ngarbagey" # True
447+ "\r\ngarbagen" # False
448+ "foo\n"
449+ )
450 stdout = tests.StringIOWrapper()
451 stderr = tests.StringIOWrapper()
452 factory = _mod_ui_text.TextUIFactory(stdin, stdout, stderr)
453@@ -154,7 +155,7 @@
454 out = test_progress._TTYStringIO()
455 self.overrideEnv('TERM', 'xterm')
456 factory = _mod_ui_text.TextUIFactory(
457- stdin=tests.StringIOWrapper("yada\ny\n"),
458+ stdin=tests.StringIOWrapper("bar\ny"),
459 stdout=out, stderr=out)
460 factory._avail_width = lambda: 79
461 pb = factory.nested_progress_bar()
462@@ -171,7 +172,7 @@
463 self.assertContainsRe(output,
464 "| foo *\r\r *\r*")
465 self.assertContainsRe(output,
466- r"what do you want\? \[y/n\]: what do you want\? \[y/n\]: ")
467+ r"what do you want\? \(\[y\]es, \[n\]o\): y")
468 # stdin should have been totally consumed
469 self.assertEqual('', factory.stdin.readline())
470
471
472=== modified file 'bzrlib/ui/__init__.py'
473--- bzrlib/ui/__init__.py 2011-05-27 05:16:48 +0000
474+++ bzrlib/ui/__init__.py 2011-09-27 19:22:41 +0000
475@@ -315,6 +315,9 @@
476 warnings.warn(fail) # so tests will fail etc
477 return fail
478
479+ def confirm(self, msg, choices, default=None):
480+ raise NotImplementedError(self.confirm)
481+
482 def get_boolean(self, prompt):
483 """Get a boolean question answered from the user.
484
485@@ -322,7 +325,8 @@
486 line without terminating \\n.
487 :return: True or False for y/yes or n/no.
488 """
489- raise NotImplementedError(self.get_boolean)
490+ char = self.confirm(prompt + '?', '&yes\n&no', default=None)
491+ return 'y' == char
492
493 def get_integer(self, prompt):
494 """Get an integer from the user.
495@@ -477,6 +481,9 @@
496 def confirm_action(self, prompt, confirmation_id, args):
497 return self.get_boolean(prompt % args)
498
499+ def confirm(self, msg, choices, default=None):
500+ return self.responses.pop(0)
501+
502 def get_boolean(self, prompt):
503 return self.responses.pop(0)
504
505
506=== modified file 'bzrlib/ui/text.py'
507--- bzrlib/ui/text.py 2011-05-16 13:39:39 +0000
508+++ bzrlib/ui/text.py 2011-09-27 19:22:41 +0000
509@@ -30,6 +30,7 @@
510
511 from bzrlib import (
512 debug,
513+ errors,
514 progress,
515 osutils,
516 trace,
517@@ -61,6 +62,40 @@
518 # paints progress, network activity, etc
519 self._progress_view = self.make_progress_view()
520
521+ def confirm(self, msg, choices, default=None):
522+ if self.stdin == sys.stdin and self.stdin.isatty():
523+ def getchar():
524+ return osutils.getchar()
525+ else:
526+ def getchar():
527+ return self.stdin.read(1)
528+
529+ shortcuts = ''
530+ alternatives = []
531+ for c in choices.split('\n'):
532+ shortcut = c.find('&')
533+ if -1 != shortcut and (shortcut + 1) < len(c):
534+ help = c[:shortcut]
535+ help += '[' + c[shortcut + 1] + ']'
536+ help += c[(shortcut + 2):]
537+ shortcut = c[shortcut + 1]
538+ else:
539+ help = c.strip('&')
540+ shortcut = c[0]
541+ shortcuts += shortcut.lower()
542+ alternatives.append(help)
543+
544+ prompt = u'%s (%s): ' % (msg, ', '.join(alternatives))
545+ self.prompt(prompt)
546+
547+ while True:
548+ char = getchar().lower()
549+ if '\r' == char and default is not None:
550+ char = default
551+ if -1 != shortcuts.find(char):
552+ self.stderr.write(char + '\n')
553+ return char
554+
555 def be_quiet(self, state):
556 if state and not self._quiet:
557 self.clear_term()
558@@ -78,18 +113,6 @@
559 # to clear it. We might need to separately check for the case of
560 self._progress_view.clear()
561
562- def get_boolean(self, prompt):
563- while True:
564- self.prompt(prompt + "? [y/n]: ")
565- line = self.stdin.readline().lower()
566- if line in ('y\n', 'yes\n'):
567- return True
568- elif line in ('n\n', 'no\n'):
569- return False
570- elif line in ('', None):
571- # end-of-file; possibly should raise an error here instead
572- return None
573-
574 def get_integer(self, prompt):
575 while True:
576 self.prompt(prompt)
577
578# Begin bundle
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
Revision history for this message
Vincent Ladeuil (vila) wrote :

> I also ran into problems using shelve with my VIM plugin.

WOW ! (And that's coming from die-hard emacs user ;-D)

> Other commands
> use ui.get_boolean, but there is no corresponding method for handling
> more complex choices. So even after providing my own UI class
> implementation, I cannot use shelve.
>
> In VIM there is a confirm function:
>
> confirm({msg} [, {choices} [, {default} [, {type}]]])
>
> Which can be used like this:
>
> confirm("Save changes?", "&Yes\n&No\n&Cancel")
>
> Where the letter after the '&' is the shortcut key for that choice.
>
> So I have been working on a branch to implement something just like
> this (see attached patch).

This sounds *seriously* interesting and I urge you to pusblish your changes
in a branch and propose it for merge so we can properly review it (I'm
pleased tht launchpad displays your patch inline ! Wow too there) but I
really want your patch to be discussed as it seems we have perfect case of
triangulation here ;)

>
> N.B.: your patch removes a call to gettext.

Ouch, yeah, I initially wrote the patch on a 2.4-based branch where the
gettext call wasn't there >-/ I'll push a revision fixing that asap. Thanks
catching it !

Revision history for this message
Martin Packman (gz) wrote :

The problem with the shelf_ui code is it does something totally different to all the other bazaar user interactions, hence the issues with qbzr and test scripts and editors apparently. Making it use bzrlib.ui should be the goal, but doesn't need to happen in this branch.

Benoit, that bundle is really exciting and apart from some nitpicking I think should basically be merged. Please do propose it as Vincent said.

Notes just on the changes in this branch:

+ char_based = not(os.environ.get('INSIDE_EMACS', None) is not None)
+ if char_based and not sys.stdin.isatty():

Space after 'not' otherwise it looks like a function. :)

Having modules one by one need to learn about magic emacs variables is a good argument for this logic living elsewhere.

+ # XXX: Warn if more than one char is typed ?

To get similar logic to the current and get_boolean behaviour, you'd take the first character that matched the set, which you can't do at this level without remembering the last line and only prompting for a new one when there are no more characters. That's pretty unimportant though.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> The problem with the shelf_ui code is it does something totally different to
> all the other bazaar user interactions, hence the issues with qbzr and test
> scripts and editors apparently. Making it use bzrlib.ui should be the goal,
> but doesn't need to happen in this branch.

/me nods

>
> Benoit, that bundle is really exciting and apart from some nitpicking I think
> should basically be merged. Please do propose it as Vincent said.

/me .. err, sorry :)

>
> Notes just on the changes in this branch:
>
> + char_based = not(os.environ.get('INSIDE_EMACS', None) is not None)
> + if char_based and not sys.stdin.isatty():
>
> Space after 'not' otherwise it looks like a function. :)

Yeah, thanks. But the plan is not to mention emacs at all there but instead
let user configure that themselves (INSIDE_EMACS is a far too big hammer and
was just a quick way to test but it's not reliable nor appropriate for all
cases), any feedback on the proposed config variables ?

>
> Having modules one by one need to learn about magic emacs variables is a good
> argument for this logic living elsewhere.
>
> + # XXX: Warn if more than one char is typed ?
>
> To get similar logic to the current and get_boolean behaviour, you'd take the
> first character that matched the set, which you can't do at this level without
> remembering the last line and only prompting for a new one when there are no
> more characters. That's pretty unimportant though.

And would keep the same UI which can drive any user nuts, it's really
intended to be line-based with one asnwer by *line* ending with CR, no
buffering of answers there.

What made shelve unsuable for me is that I needed to prepare the answers for
all the questions *before* seeing the questions, keeping this logic just
don't work ;)

So my question may be clearer as:

- given that we expect only a single char (followed by a \n), what should we
  do if we get more ? Warn ? Swallow silently ? Fail ?

Also, I need to add tests and that will probably remove the weird

  char = '\n'

just-in-case hack.

review: Needs Information
Revision history for this message
Martin Packman (gz) wrote :

So, to be clear, I think this branch may as well land as an improvement to the current isatty() hack, with the thought that Benoit's change will be the basis for a longer term solution.

> > + char_based = not(os.environ.get('INSIDE_EMACS', None) is not None)

...or just `is None` for that matter.

> Yeah, thanks. But the plan is not to mention emacs at all there but instead
> let user configure that themselves (INSIDE_EMACS is a far too big hammer and
> was just a quick way to test but it's not reliable nor appropriate for all
> cases), any feedback on the proposed config variables ?

For now it seems an appropriate hammer, just hang it elsewhere in a little private function. After this is all moved to bzrlib.ui then a config value to enable/disable character based input seems reasonable, but I wouldn't bother for now.

> - given that we expect only a single char (followed by a \n), what should we
> do if we get more ? Warn ? Swallow silently ? Fail ?

Swallow (and maybe warn) or fail and reprompt would all be fine.

> Also, I need to add tests and that will probably remove the weird
>
> char = '\n'
>
> just-in-case hack.

Right, you probably want a test for the EOF case, it needs to make sure it gets out of the shelf loop somehow.

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

sent to pqm by email

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/shelf_ui.py'
2--- bzrlib/shelf_ui.py 2011-09-16 15:39:47 +0000
3+++ bzrlib/shelf_ui.py 2011-09-28 15:10:33 +0000
4@@ -16,6 +16,7 @@
5
6
7 from cStringIO import StringIO
8+import os
9 import shutil
10 import sys
11 import tempfile
12@@ -251,20 +252,41 @@
13 diff_file.seek(0)
14 return patches.parse_patch(diff_file)
15
16+ def _char_based(self):
17+ # FIXME: A bit hackish to use INSIDE_EMACS here, but there is another
18+ # work in progress moving this method (and more importantly prompt()
19+ # below) into the ui area and address the issue in better ways.
20+ # -- vila 2011-09-28
21+ return os.environ.get('INSIDE_EMACS', None) is None
22+
23 def prompt(self, message):
24 """Prompt the user for a character.
25
26 :param message: The message to prompt a user with.
27 :return: A character.
28 """
29- if not sys.stdin.isatty():
30- # Since there is no controlling terminal we will hang when trying
31- # to prompt the user, better abort now. See
32+ char_based = self._char_based()
33+ if char_based and not sys.stdin.isatty():
34+ # Since there is no controlling terminal we will hang when
35+ # trying to prompt the user, better abort now. See
36 # https://code.launchpad.net/~bialix/bzr/shelve-no-tty/+merge/14905
37 # for more context.
38 raise errors.BzrError(gettext("You need a controlling terminal."))
39 sys.stdout.write(message)
40- char = osutils.getchar()
41+ if char_based:
42+ # We peek one char at a time which requires a real term here
43+ char = osutils.getchar()
44+ else:
45+ # While running tests (or under emacs) the input is line buffered
46+ # so we must not use osutils.getchar(). Instead we switch to a mode
47+ # where each line is terminated by a new line
48+ line = sys.stdin.readline()
49+ if line:
50+ # XXX: Warn if more than one char is typed ?
51+ char = line[0]
52+ else:
53+ # Empty input, callers handle it as enter
54+ char = ''
55 sys.stdout.write("\r" + ' ' * len(message) + '\r')
56 sys.stdout.flush()
57 return char
58
59=== modified file 'bzrlib/tests/script.py'
60--- bzrlib/tests/script.py 2011-01-12 01:01:53 +0000
61+++ bzrlib/tests/script.py 2011-09-28 15:10:33 +0000
62@@ -481,6 +481,10 @@
63 def setUp(self):
64 super(TestCaseWithMemoryTransportAndScript, self).setUp()
65 self.script_runner = ScriptRunner()
66+ # FIXME: See shelf_ui.Shelver._char_based. This allow using shelve in
67+ # scripts while providing a line-based input (better solution in
68+ # progress). -- vila 2011-09-28
69+ self.overrideEnv('INSIDE_EMACS', '1')
70
71 def run_script(self, script, null_output_matches_anything=False):
72 return self.script_runner.run_script(self, script,
73@@ -511,6 +515,10 @@
74 def setUp(self):
75 super(TestCaseWithTransportAndScript, self).setUp()
76 self.script_runner = ScriptRunner()
77+ # FIXME: See shelf_ui.Shelver._char_based. This allow using shelve in
78+ # scripts while providing a line-based input (better solution in
79+ # progress). -- vila 2011-09-28
80+ self.overrideEnv('INSIDE_EMACS', '1')
81
82 def run_script(self, script, null_output_matches_anything=False):
83 return self.script_runner.run_script(self, script,
84
85=== modified file 'bzrlib/tests/test_script.py'
86--- bzrlib/tests/test_script.py 2011-05-16 13:39:39 +0000
87+++ bzrlib/tests/test_script.py 2011-09-28 15:10:33 +0000
88@@ -572,9 +572,9 @@
89 def test_confirm_action(self):
90 """You can write tests that demonstrate user confirmation.
91
92- Specifically, ScriptRunner does't care if the output line for the prompt
93- isn't terminated by a newline from the program; it's implicitly terminated
94- by the input.
95+ Specifically, ScriptRunner does't care if the output line for the
96+ prompt isn't terminated by a newline from the program; it's implicitly
97+ terminated by the input.
98 """
99 commands.builtin_command_registry.register(cmd_test_confirm)
100 self.addCleanup(commands.builtin_command_registry.remove, 'test-confirm')
101@@ -589,3 +589,56 @@
102 ok, no
103 """)
104
105+class TestShelve(script.TestCaseWithTransportAndScript):
106+
107+ def setUp(self):
108+ super(TestShelve, self).setUp()
109+ self.run_script("""
110+ $ bzr init test
111+ Created a standalone tree (format: 2a)
112+ $ cd test
113+ $ echo foo > file
114+ $ bzr add
115+ adding file
116+ $ bzr commit -m 'file added'
117+ 2>Committing to:...test/
118+ 2>added file
119+ 2>Committed revision 1.
120+ $ echo bar > file
121+ """)
122+
123+ def test_shelve(self):
124+ self.run_script("""
125+ $ bzr shelve -m 'shelve bar'
126+ # Shelve? [yNfq?]
127+ <y
128+ # Shelve 1 change(s)? [yNfq?]
129+ <y
130+ 2>Selected changes:
131+ 2> M file
132+ 2>Changes shelved with id "1".
133+ """,
134+ # shelve uses \r that can't be represented in the
135+ # script ?
136+ null_output_matches_anything=True)
137+ self.run_script("""
138+ $ bzr shelve --list
139+ 1: shelve bar
140+ """)
141+
142+ def test_dont_shelve(self):
143+ # We intentionally provide no input here to test EOF
144+ self.run_script("""
145+ $ bzr shelve -m 'shelve bar'
146+ # Shelve? [yNfq?]
147+ # Shelve 1 change(s)? [yNfq?]
148+ 2>No changes to shelve.
149+ """,
150+ # shelve uses \r that can't be represented in the
151+ # script ?
152+ null_output_matches_anything=True)
153+ self.run_script("""
154+ $ bzr st
155+ modified:
156+ file
157+ """)
158
159=== modified file 'doc/en/release-notes/bzr-2.5.txt'
160--- doc/en/release-notes/bzr-2.5.txt 2011-09-27 10:17:36 +0000
161+++ doc/en/release-notes/bzr-2.5.txt 2011-09-28 15:10:33 +0000
162@@ -45,6 +45,10 @@
163 .. Fixes for situations where bzr would previously crash or give incorrect
164 or undesirable results.
165
166+* ``bzr shelve`` can now be used in emacs shells as the input handling is
167+ turned into a line-basde one when ``INSIDE_EMACS`` is set (which is the
168+ case for all recent emacs versions). (Vincent Ladeuil, #856261)
169+
170 * Redirects between http and https no longer discard path information
171 in some cases. (Jelmer Vernooij, #853765)
172
173@@ -87,6 +91,9 @@
174 Testing
175 *******
176
177+* Test scripts can now use ``bzr shelve`` and provide their input as
178+ complete lines. (Vincent Ladeuil, #856261)
179+
180 .. Fixes and changes that are only relevant to bzr's test framework and
181 suite. This can include new facilities for writing tests, fixes to
182 spurious test failures and changes to the way things should be tested.