Strip leading or trailing '-' when generating patch names
[stgit] / stgit / argparse.py
CommitLineData
575bbdae
KH
1"""This module provides a layer on top of the standard library's
2C{optparse} module, so that we can easily generate both interactive
3help and asciidoc documentation (such as man pages)."""
20a52e06 4
575bbdae 5import optparse, sys, textwrap
20a52e06
KH
6from stgit import utils
7from stgit.config import config
8
575bbdae
KH
9def _splitlist(lst, split_on):
10 """Iterate over the sublists of lst that are separated by an element e
11 such that split_on(e) is true."""
12 current = []
13 for e in lst:
14 if split_on(e):
15 yield current
16 current = []
17 else:
18 current.append(e)
19 yield current
20
21def _paragraphs(s):
22 """Split a string s into a list of paragraphs, each of which is a list
23 of lines."""
24 lines = [line.rstrip() for line in textwrap.dedent(s).strip().splitlines()]
25 return [p for p in _splitlist(lines, lambda line: not line.strip()) if p]
26
27class opt(object):
28 """Represents a command-line flag."""
6c8a90e1
KH
29 def __init__(self, *pargs, **kwargs):
30 self.pargs = pargs
575bbdae
KH
31 self.kwargs = kwargs
32 def get_option(self):
33 kwargs = dict(self.kwargs)
34 kwargs['help'] = kwargs['short']
6c8a90e1
KH
35 for k in ['short', 'long', 'args']:
36 kwargs.pop(k, None)
37 return optparse.make_option(*self.pargs, **kwargs)
575bbdae
KH
38 def metavar(self):
39 o = self.get_option()
6f775ac9 40 if not o.takes_value():
575bbdae
KH
41 return None
42 if o.metavar:
43 return o.metavar
6c8a90e1 44 for flag in self.pargs:
575bbdae
KH
45 if flag.startswith('--'):
46 return utils.strip_prefix('--', flag).upper()
47 raise Exception('Cannot determine metavar')
48 def write_asciidoc(self, f):
6c8a90e1 49 for flag in self.pargs:
575bbdae
KH
50 f.write(flag)
51 m = self.metavar()
52 if m:
53 f.write(' ' + m)
54 f.write('::\n')
55 paras = _paragraphs(self.kwargs.get('long', self.kwargs['short'] + '.'))
56 for line in paras[0]:
57 f.write(' '*8 + line + '\n')
58 for para in paras[1:]:
59 f.write('+\n')
60 for line in para:
61 f.write(line + '\n')
6c8a90e1
KH
62 @property
63 def flags(self):
64 return self.pargs
65 @property
66 def args(self):
67 if self.kwargs.get('action', None) in ['store_true', 'store_false']:
68 default = []
69 else:
70 default = [files]
71 return self.kwargs.get('args', default)
575bbdae
KH
72
73def _cmd_name(cmd_mod):
74 return getattr(cmd_mod, 'name', cmd_mod.__name__.split('.')[-1])
75
76def make_option_parser(cmd):
77 pad = ' '*len('Usage: ')
78 return optparse.OptionParser(
79 prog = 'stg %s' % _cmd_name(cmd),
80 usage = (('\n' + pad).join('%%prog %s' % u for u in cmd.usage) +
81 '\n\n' + cmd.help),
82 option_list = [o.get_option() for o in cmd.options])
83
84def _write_underlined(s, u, f):
85 f.write(s + '\n')
86 f.write(u*len(s) + '\n')
87
88def write_asciidoc(cmd, f):
89 _write_underlined('stg-%s(1)' % _cmd_name(cmd), '=', f)
90 f.write('\n')
91 _write_underlined('NAME', '-', f)
92 f.write('stg-%s - %s\n\n' % (_cmd_name(cmd), cmd.help))
93 _write_underlined('SYNOPSIS', '-', f)
94 f.write('[verse]\n')
95 for u in cmd.usage:
96 f.write("'stg' %s %s\n" % (_cmd_name(cmd), u))
97 f.write('\n')
98 _write_underlined('DESCRIPTION', '-', f)
99 f.write('\n%s\n\n' % cmd.description.strip('\n'))
100 if cmd.options:
101 _write_underlined('OPTIONS', '-', f)
102 for o in cmd.options:
103 o.write_asciidoc(f)
104 f.write('\n')
105 _write_underlined('StGit', '-', f)
9777fa36 106 f.write('Part of the StGit suite - see linkman:stg[1]\n')
575bbdae 107
20a52e06
KH
108def sign_options():
109 def callback(option, opt_str, value, parser, sign_str):
110 if parser.values.sign_str not in [None, sign_str]:
111 raise optparse.OptionValueError(
112 '--ack and --sign were both specified')
113 parser.values.sign_str = sign_str
575bbdae 114 return [
6c8a90e1 115 opt('--sign', action = 'callback', dest = 'sign_str', args = [],
575bbdae
KH
116 callback = callback, callback_args = ('Signed-off-by',),
117 short = 'Add "Signed-off-by:" line', long = """
118 Add a "Signed-off-by:" to the end of the patch."""),
6c8a90e1 119 opt('--ack', action = 'callback', dest = 'sign_str', args = [],
575bbdae
KH
120 callback = callback, callback_args = ('Acked-by',),
121 short = 'Add "Acked-by:" line', long = """
122 Add an "Acked-by:" line to the end of the patch.""")]
20a52e06 123
f9d69fc4 124def message_options(save_template):
20a52e06
KH
125 def no_dup(parser):
126 if parser.values.message != None:
127 raise optparse.OptionValueError(
128 'Cannot give more than one --message or --file')
129 def no_combine(parser):
f9d69fc4 130 if (save_template and parser.values.message != None
20a52e06
KH
131 and parser.values.save_template != None):
132 raise optparse.OptionValueError(
133 'Cannot give both --message/--file and --save-template')
134 def msg_callback(option, opt_str, value, parser):
135 no_dup(parser)
136 parser.values.message = value
137 no_combine(parser)
138 def file_callback(option, opt_str, value, parser):
139 no_dup(parser)
140 if value == '-':
141 parser.values.message = sys.stdin.read()
142 else:
143 f = file(value)
144 parser.values.message = f.read()
145 f.close()
146 no_combine(parser)
147 def templ_callback(option, opt_str, value, parser):
148 if value == '-':
149 def w(s):
150 sys.stdout.write(s)
151 else:
152 def w(s):
153 f = file(value, 'w+')
154 f.write(s)
155 f.close()
156 parser.values.save_template = w
157 no_combine(parser)
f9d69fc4 158 opts = [
575bbdae
KH
159 opt('-m', '--message', action = 'callback',
160 callback = msg_callback, dest = 'message', type = 'string',
161 short = 'Use MESSAGE instead of invoking the editor'),
162 opt('-f', '--file', action = 'callback', callback = file_callback,
6c8a90e1 163 dest = 'message', type = 'string', args = [files],
9ba661f6 164 metavar = 'FILE',
575bbdae
KH
165 short = 'Use FILE instead of invoking the editor', long = """
166 Use the contents of FILE instead of invoking the editor.
f9d69fc4
KH
167 (If FILE is "-", write to stdout.)""")]
168 if save_template:
169 opts.append(
170 opt('--save-template', action = 'callback', dest = 'save_template',
171 callback = templ_callback, metavar = 'FILE', type = 'string',
172 short = 'Save the message template to FILE and exit', long = """
173 Instead of running the command, just write the message
174 template to FILE, and exit. (If FILE is "-", write to
175 stdout.)
575bbdae 176
f9d69fc4
KH
177 When driving StGit from another program, it is often
178 useful to first call a command with '--save-template',
179 then let the user edit the message, and then call the
180 same command with '--file'."""))
181 return opts
20a52e06
KH
182
183def diff_opts_option():
184 def diff_opts_callback(option, opt_str, value, parser):
185 if value:
186 parser.values.diff_flags.extend(value.split())
187 else:
188 parser.values.diff_flags = []
575bbdae
KH
189 return [
190 opt('-O', '--diff-opts', dest = 'diff_flags',
191 default = (config.get('stgit.diff-opts') or '').split(),
192 action = 'callback', callback = diff_opts_callback,
193 type = 'string', metavar = 'OPTIONS',
6c8a90e1 194 args = [strings('-M', '-C')],
575bbdae 195 short = 'Extra options to pass to "git diff"')]
20a52e06 196
575bbdae 197def _person_opts(person, short):
20a52e06
KH
198 """Sets options.<person> to a function that modifies a Person
199 according to the commandline options."""
200 def short_callback(option, opt_str, value, parser, field):
201 f = getattr(parser.values, person)
202 setattr(parser.values, person,
203 lambda p: getattr(f(p), 'set_' + field)(value))
204 def full_callback(option, opt_str, value, parser):
205 ne = utils.parse_name_email(value)
206 if not ne:
207 raise optparse.OptionValueError(
208 'Bad %s specification: %r' % (opt_str, value))
209 name, email = ne
210 short_callback(option, opt_str, name, parser, 'name')
211 short_callback(option, opt_str, email, parser, 'email')
575bbdae
KH
212 return (
213 [opt('--%s' % person, metavar = '"NAME <EMAIL>"', type = 'string',
214 action = 'callback', callback = full_callback, dest = person,
215 default = lambda p: p, short = 'Set the %s details' % person)] +
216 [opt('--%s%s' % (short, f), metavar = f.upper(), type = 'string',
217 action = 'callback', callback = short_callback, dest = person,
218 callback_args = (f,), short = 'Set the %s %s' % (person, f))
219 for f in ['name', 'email', 'date']])
20a52e06 220
f9d69fc4
KH
221def author_options():
222 return _person_opts('author', 'auth')
223
ee11a289
CM
224def keep_option():
225 return [opt('-k', '--keep', action = 'store_true',
226 short = 'Keep the local changes',
227 default = config.get('stgit.autokeep') == 'yes')]
228
b4d91eee
CM
229def merged_option():
230 return [opt('-m', '--merged', action = 'store_true',
231 short = 'Check for patches merged upstream')]
232
6c8a90e1
KH
233class CompgenBase(object):
234 def actions(self, var): return set()
235 def words(self, var): return set()
236 def command(self, var):
237 cmd = ['compgen']
238 for act in self.actions(var):
239 cmd += ['-A', act]
240 words = self.words(var)
241 if words:
242 cmd += ['-W', '"%s"' % ' '.join(words)]
243 cmd += ['--', '"%s"' % var]
244 return ' '.join(cmd)
245
246class CompgenJoin(CompgenBase):
247 def __init__(self, a, b):
248 assert isinstance(a, CompgenBase)
249 assert isinstance(b, CompgenBase)
250 self.__a = a
251 self.__b = b
252 def words(self, var): return self.__a.words(var) | self.__b.words(var)
253 def actions(self, var): return self.__a.actions(var) | self.__b.actions(var)
254
255class Compgen(CompgenBase):
256 def __init__(self, words = frozenset(), actions = frozenset()):
257 self.__words = set(words)
258 self.__actions = set(actions)
259 def actions(self, var): return self.__actions
260 def words(self, var): return self.__words
261
262def compjoin(compgens):
263 comp = Compgen()
264 for c in compgens:
265 comp = CompgenJoin(comp, c)
266 return comp
267
268all_branches = Compgen(['$(_all_branches)'])
269stg_branches = Compgen(['$(_stg_branches)'])
270applied_patches = Compgen(['$(_applied_patches)'])
271other_applied_patches = Compgen(['$(_other_applied_patches)'])
272unapplied_patches = Compgen(['$(_unapplied_patches)'])
273hidden_patches = Compgen(['$(_hidden_patches)'])
274commit = Compgen(['$(_all_branches) $(_tags) $(_remotes)'])
275conflicting_files = Compgen(['$(_conflicting_files)'])
276dirty_files = Compgen(['$(_dirty_files)'])
277unknown_files = Compgen(['$(_unknown_files)'])
278known_files = Compgen(['$(_known_files)'])
279repo = Compgen(actions = ['directory'])
280dir = Compgen(actions = ['directory'])
281files = Compgen(actions = ['file'])
282def strings(*ss): return Compgen(ss)
283class patch_range(CompgenBase):
284 def __init__(self, *endpoints):
285 self.__endpoints = endpoints
286 def words(self, var):
287 words = set()
288 for e in self.__endpoints:
289 assert not e.actions(var)
290 words |= e.words(var)
291 return set(['$(_patch_range "%s" "%s")' % (' '.join(words), var)])