stg import now extracts Message-ID header
[stgit.git] / stgit / argparse.py
blob63cdb3fefa9111cbdc9edf14b62b079e06c9f098
1 """Command line argument parsing for StGit subcommands.
3 This module provides a layer on top of the standard library's :mod:`optparse` to
4 facilitate generation of both interactive help and asciidoc documentation (such as man
5 pages).
7 """
9 import io
10 import optparse
11 import sys
12 import textwrap
14 from stgit import utils
15 from stgit.config import config
16 from stgit.lib.git import Date
17 from stgit.out import out
20 def _splitlist(lst, split_on):
21 """Split list using provided predicate."""
22 current = []
23 for e in lst:
24 if split_on(e):
25 yield current
26 current = []
27 else:
28 current.append(e)
29 yield current
32 def _paragraphs(s):
33 """Split a string s into a list of paragraphs, each of which is a list of lines."""
34 lines = [line.rstrip() for line in textwrap.dedent(s).strip().splitlines()]
35 return [p for p in _splitlist(lines, lambda line: not line.strip()) if p]
38 class opt:
39 """Represents a command-line flag."""
41 def __init__(self, *pargs, **kwargs):
42 self.pargs = pargs
43 self.kwargs = kwargs
45 def get_option(self):
46 kwargs = dict(self.kwargs)
47 kwargs['help'] = kwargs['short']
48 for k in ['short', 'long', 'args']:
49 kwargs.pop(k, None)
50 return optparse.make_option(*self.pargs, **kwargs)
52 def metavar(self):
53 o = self.get_option()
54 if not o.takes_value():
55 return None
56 if o.metavar:
57 return o.metavar
58 for flag in self.pargs:
59 if flag.startswith('--'):
60 return utils.strip_prefix('--', flag).upper()
61 raise Exception('Cannot determine metavar')
63 def write_asciidoc(self, f):
64 for flag in self.pargs:
65 f.write(flag)
66 m = self.metavar()
67 if m:
68 f.write(' ' + m)
69 f.write('::\n')
70 paras = _paragraphs(self.kwargs.get('long', self.kwargs['short'] + '.'))
71 for line in paras[0]:
72 f.write(' ' * 8 + line + '\n')
73 for para in paras[1:]:
74 f.write('+\n')
75 for line in para:
76 f.write(line + '\n')
78 @property
79 def flags(self):
80 return self.pargs
82 @property
83 def args(self):
84 if self.kwargs.get('action', None) in ['store_true', 'store_false']:
85 default = []
86 else:
87 default = ['files']
88 return self.kwargs.get('args', default)
91 def _cmd_name(cmd_mod):
92 return getattr(cmd_mod, 'name', cmd_mod.__name__.split('.')[-1])
95 def make_option_parser(cmd):
96 pad = ' ' * len('Usage: ')
97 return optparse.OptionParser(
98 prog='stg %s' % _cmd_name(cmd),
99 usage=(
100 ('\n' + pad).join('%%prog %s' % u for u in cmd.usage) + '\n\n' + cmd.help
102 option_list=[o.get_option() for o in cmd.options],
106 def _write_underlined(s, u, f):
107 f.write(s + '\n')
108 f.write(u * len(s) + '\n')
111 def write_asciidoc(cmd, f):
112 _write_underlined('stg-%s(1)' % _cmd_name(cmd), '=', f)
113 f.write('\n')
114 _write_underlined('NAME', '-', f)
115 f.write('stg-%s - %s\n\n' % (_cmd_name(cmd), cmd.help))
116 _write_underlined('SYNOPSIS', '-', f)
117 f.write('[verse]\n')
118 for u in cmd.usage:
119 f.write("'stg %s' %s\n" % (_cmd_name(cmd), u))
120 f.write('\n')
121 _write_underlined('DESCRIPTION', '-', f)
122 f.write('\n%s\n\n' % cmd.description.strip('\n'))
123 if cmd.options:
124 _write_underlined('OPTIONS', '-', f)
125 for o in cmd.options:
126 o.write_asciidoc(f)
127 f.write('\n')
128 _write_underlined('StGit', '-', f)
129 f.write('Part of the StGit suite - see linkman:stg[1]\n')
132 def trailer_options():
133 def callback(option, opt_str, value, parser, trailer):
134 parser.values.trailers.append((trailer, None))
136 def by_callback(option, opt_str, value, parser, trailer):
137 parser.values.trailers.append((trailer, value))
139 return [
140 opt(
141 '--sign',
142 action='callback',
143 dest='trailers',
144 default=[],
145 callback=callback,
146 callback_args=('Signed-off-by',),
147 short='Add "Signed-off-by:" trailer',
148 long=(
149 'Add a "Signed-off-by:" trailer to the end of the message using the '
150 'committer name and email for the trailer value.'
153 opt(
154 '--sign-by',
155 action='callback',
156 dest='trailers',
157 default=[],
158 callback=by_callback,
159 callback_args=('Signed-off-by',),
160 type='string',
161 metavar='VALUE',
162 short='Add "Signed-off-by:" trailer with custom VALUE',
163 long=(
164 'Add a "Signed-off-by:" trailer with a custom VALUE to the end of the '
165 'message.'
168 opt(
169 '--ack',
170 action='callback',
171 dest='trailers',
172 default=[],
173 callback=callback,
174 callback_args=('Acked-by',),
175 short='Add "Acked-by:" trailer',
176 long=(
177 'Add an "Acked-by:" trailer to the end of the message using the '
178 'commiter name and email for the trailer value.'
181 opt(
182 '--ack-by',
183 action='callback',
184 dest='trailers',
185 default=[],
186 callback=by_callback,
187 callback_args=('Acked-by',),
188 type='string',
189 metavar='VALUE',
190 short='Add "Acked-by:" trailer with custom VALUE',
191 long=(
192 'Add an "Acked-by:" trailer with a custom VALUE to the end of the '
193 'message.'
196 opt(
197 '--review',
198 action='callback',
199 dest='trailers',
200 default=[],
201 callback=callback,
202 callback_args=('Reviewed-by',),
203 short='Add "Reviewed-by:" trailer',
204 long=(
205 'Add a "Reviewed-by:" trailer to the end of the message using the '
206 'commiter name and email for the trailer value.'
209 opt(
210 '--review-by',
211 action='callback',
212 dest='trailers',
213 default=[],
214 callback=by_callback,
215 callback_args=('Reviewed-by',),
216 type='string',
217 metavar='VALUE',
218 short='Add "Reviewed-by:" trailer with custom VALUE',
219 long=(
220 'Add a "Reviewed-by:" trailer with custom VALUE to the end of the '
221 'message.'
227 def hook_options():
228 return [
229 opt(
230 '--no-verify',
231 action='store_true',
232 dest='no_verify',
233 default=False,
234 short='Disable commit-msg hook',
235 long="This option bypasses the commit-msg hook.",
240 def message_options(save_template):
241 def no_dup(parser):
242 if parser.values.message is not None:
243 raise optparse.OptionValueError(
244 'Cannot give more than one --message or --file'
247 def no_combine(parser):
248 if (
249 save_template
250 and parser.values.message is not None
251 and parser.values.save_template is not None
253 raise optparse.OptionValueError(
254 'Cannot give both --message/--file and --save-template'
257 def msg_callback(option, opt_str, value, parser):
258 no_dup(parser)
259 if value and not value.endswith('\n'):
260 value += '\n'
261 parser.values.message = value
262 no_combine(parser)
264 def file_callback(option, opt_str, value, parser):
265 no_dup(parser)
266 if value == '-':
267 parser.values.message = sys.stdin.read()
268 else:
269 with open(value) as f:
270 parser.values.message = f.read()
271 no_combine(parser)
273 def templ_callback(option, opt_str, value, parser):
274 if value == '-':
275 parser.values.save_template = out.stdout_bytes
276 else:
278 def write_file(s):
279 with io.open(value, 'wb') as f:
280 f.write(s)
282 parser.values.save_template = write_file
283 no_combine(parser)
285 opts = [
286 opt(
287 '-m',
288 '--message',
289 action='callback',
290 callback=msg_callback,
291 dest='message',
292 type='string',
293 short='Use MESSAGE instead of invoking the editor',
295 opt(
296 '-f',
297 '--file',
298 action='callback',
299 callback=file_callback,
300 dest='message',
301 type='string',
302 args=['files'],
303 metavar='FILE',
304 short='Use FILE instead of invoking the editor',
305 long="""
306 Use the contents of FILE instead of invoking the editor.
307 (If FILE is "-", write to stdout.)""",
310 if save_template:
311 opts.append(
312 opt(
313 '--save-template',
314 action='callback',
315 dest='save_template',
316 callback=templ_callback,
317 metavar='FILE',
318 type='string',
319 short='Save the message template to FILE and exit',
320 long="""
321 Instead of running the command, just write the message
322 template to FILE, and exit. (If FILE is "-", write to
323 stdout.)
325 When driving StGit from another program, it is often
326 useful to first call a command with '--save-template',
327 then let the user edit the message, and then call the
328 same command with '--file'.""",
331 return opts
334 def diff_opts_option():
335 def diff_opts_callback(option, opt_str, value, parser):
336 if value:
337 parser.values.diff_flags.extend(value.split())
338 else:
339 parser.values.diff_flags = []
341 return [
342 opt(
343 '-O',
344 '--diff-opts',
345 dest='diff_flags',
346 default=(config.get('stgit.diff-opts') or '').split(),
347 action='callback',
348 callback=diff_opts_callback,
349 type='string',
350 metavar='OPTIONS',
351 args=[strings('-M', '-C')],
352 short='Extra options to pass to "git diff"',
357 def author_options():
358 """Create command line options for setting author information.
360 The ``opts.author`` destination variable is a callback function that modifies a
361 :class:`Person` according to the these command line options.
365 def short_callback(option, opt_str, value, parser, field):
366 f = parser.values.author
367 if field == "date":
368 value = Date(value)
369 parser.values.author = lambda p: getattr(f(p), 'set_' + field)(value)
371 def full_callback(option, opt_str, value, parser):
372 ne = utils.parse_name_email(value)
373 if not ne:
374 raise optparse.OptionValueError(
375 'Bad %s specification: %r' % (opt_str, value)
377 name, email = ne
378 short_callback(option, opt_str, name, parser, 'name')
379 short_callback(option, opt_str, email, parser, 'email')
381 return [
382 opt(
383 '--author',
384 metavar='"NAME <EMAIL>"',
385 type='string',
386 action='callback',
387 callback=full_callback,
388 dest='author',
389 default=lambda p: p,
390 short='Set the author details',
392 ] + [
393 opt(
394 '--auth%s' % field,
395 metavar=field.upper(),
396 type='string',
397 action='callback',
398 callback=short_callback,
399 dest='author',
400 callback_args=(field,),
401 short='Set the author %s' % field,
403 for field in ['name', 'email', 'date']
407 def keep_option():
408 return [
409 opt(
410 '-k',
411 '--keep',
412 action='store_true',
413 short='Keep the local changes',
414 default=config.get('stgit.autokeep') == 'yes',
419 def merged_option():
420 return [
421 opt(
422 '-m',
423 '--merged',
424 action='store_true',
425 short='Check for patches merged upstream',
430 class strings(list):
431 def __init__(self, *args):
432 super().__init__(args)
435 class patch_range(list):
436 def __init__(self, *args):
437 super().__init__(args)