Commit | Line | Data |
---|---|---|
575bbdae KH |
1 | """This module provides a layer on top of the standard library's |
2 | C{optparse} module, so that we can easily generate both interactive | |
3 | help and asciidoc documentation (such as man pages).""" | |
20a52e06 | 4 | |
575bbdae | 5 | import optparse, sys, textwrap |
20a52e06 KH |
6 | from stgit import utils |
7 | from stgit.config import config | |
8 | ||
575bbdae KH |
9 | def _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 | ||
21 | def _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 | ||
27 | class 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 | |
73 | def _cmd_name(cmd_mod): | |
74 | return getattr(cmd_mod, 'name', cmd_mod.__name__.split('.')[-1]) | |
75 | ||
76 | def 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 | ||
84 | def _write_underlined(s, u, f): | |
85 | f.write(s + '\n') | |
86 | f.write(u*len(s) + '\n') | |
87 | ||
88 | def 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) | |
d3c560ba | 106 | f.write('Part of the StGit suite - see manlink:stg[1]\n') |
575bbdae | 107 | |
20a52e06 KH |
108 | def 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 | 124 | def 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], |
575bbdae KH |
164 | short = 'Use FILE instead of invoking the editor', long = """ |
165 | Use the contents of FILE instead of invoking the editor. | |
f9d69fc4 KH |
166 | (If FILE is "-", write to stdout.)""")] |
167 | if save_template: | |
168 | opts.append( | |
169 | opt('--save-template', action = 'callback', dest = 'save_template', | |
170 | callback = templ_callback, metavar = 'FILE', type = 'string', | |
171 | short = 'Save the message template to FILE and exit', long = """ | |
172 | Instead of running the command, just write the message | |
173 | template to FILE, and exit. (If FILE is "-", write to | |
174 | stdout.) | |
575bbdae | 175 | |
f9d69fc4 KH |
176 | When driving StGit from another program, it is often |
177 | useful to first call a command with '--save-template', | |
178 | then let the user edit the message, and then call the | |
179 | same command with '--file'.""")) | |
180 | return opts | |
20a52e06 KH |
181 | |
182 | def diff_opts_option(): | |
183 | def diff_opts_callback(option, opt_str, value, parser): | |
184 | if value: | |
185 | parser.values.diff_flags.extend(value.split()) | |
186 | else: | |
187 | parser.values.diff_flags = [] | |
575bbdae KH |
188 | return [ |
189 | opt('-O', '--diff-opts', dest = 'diff_flags', | |
190 | default = (config.get('stgit.diff-opts') or '').split(), | |
191 | action = 'callback', callback = diff_opts_callback, | |
192 | type = 'string', metavar = 'OPTIONS', | |
6c8a90e1 | 193 | args = [strings('-M', '-C')], |
575bbdae | 194 | short = 'Extra options to pass to "git diff"')] |
20a52e06 | 195 | |
575bbdae | 196 | def _person_opts(person, short): |
20a52e06 KH |
197 | """Sets options.<person> to a function that modifies a Person |
198 | according to the commandline options.""" | |
199 | def short_callback(option, opt_str, value, parser, field): | |
200 | f = getattr(parser.values, person) | |
201 | setattr(parser.values, person, | |
202 | lambda p: getattr(f(p), 'set_' + field)(value)) | |
203 | def full_callback(option, opt_str, value, parser): | |
204 | ne = utils.parse_name_email(value) | |
205 | if not ne: | |
206 | raise optparse.OptionValueError( | |
207 | 'Bad %s specification: %r' % (opt_str, value)) | |
208 | name, email = ne | |
209 | short_callback(option, opt_str, name, parser, 'name') | |
210 | short_callback(option, opt_str, email, parser, 'email') | |
575bbdae KH |
211 | return ( |
212 | [opt('--%s' % person, metavar = '"NAME <EMAIL>"', type = 'string', | |
213 | action = 'callback', callback = full_callback, dest = person, | |
214 | default = lambda p: p, short = 'Set the %s details' % person)] + | |
215 | [opt('--%s%s' % (short, f), metavar = f.upper(), type = 'string', | |
216 | action = 'callback', callback = short_callback, dest = person, | |
217 | callback_args = (f,), short = 'Set the %s %s' % (person, f)) | |
218 | for f in ['name', 'email', 'date']]) | |
20a52e06 | 219 | |
f9d69fc4 KH |
220 | def author_options(): |
221 | return _person_opts('author', 'auth') | |
222 | ||
6c8a90e1 KH |
223 | class CompgenBase(object): |
224 | def actions(self, var): return set() | |
225 | def words(self, var): return set() | |
226 | def command(self, var): | |
227 | cmd = ['compgen'] | |
228 | for act in self.actions(var): | |
229 | cmd += ['-A', act] | |
230 | words = self.words(var) | |
231 | if words: | |
232 | cmd += ['-W', '"%s"' % ' '.join(words)] | |
233 | cmd += ['--', '"%s"' % var] | |
234 | return ' '.join(cmd) | |
235 | ||
236 | class CompgenJoin(CompgenBase): | |
237 | def __init__(self, a, b): | |
238 | assert isinstance(a, CompgenBase) | |
239 | assert isinstance(b, CompgenBase) | |
240 | self.__a = a | |
241 | self.__b = b | |
242 | def words(self, var): return self.__a.words(var) | self.__b.words(var) | |
243 | def actions(self, var): return self.__a.actions(var) | self.__b.actions(var) | |
244 | ||
245 | class Compgen(CompgenBase): | |
246 | def __init__(self, words = frozenset(), actions = frozenset()): | |
247 | self.__words = set(words) | |
248 | self.__actions = set(actions) | |
249 | def actions(self, var): return self.__actions | |
250 | def words(self, var): return self.__words | |
251 | ||
252 | def compjoin(compgens): | |
253 | comp = Compgen() | |
254 | for c in compgens: | |
255 | comp = CompgenJoin(comp, c) | |
256 | return comp | |
257 | ||
258 | all_branches = Compgen(['$(_all_branches)']) | |
259 | stg_branches = Compgen(['$(_stg_branches)']) | |
260 | applied_patches = Compgen(['$(_applied_patches)']) | |
261 | other_applied_patches = Compgen(['$(_other_applied_patches)']) | |
262 | unapplied_patches = Compgen(['$(_unapplied_patches)']) | |
263 | hidden_patches = Compgen(['$(_hidden_patches)']) | |
264 | commit = Compgen(['$(_all_branches) $(_tags) $(_remotes)']) | |
265 | conflicting_files = Compgen(['$(_conflicting_files)']) | |
266 | dirty_files = Compgen(['$(_dirty_files)']) | |
267 | unknown_files = Compgen(['$(_unknown_files)']) | |
268 | known_files = Compgen(['$(_known_files)']) | |
269 | repo = Compgen(actions = ['directory']) | |
270 | dir = Compgen(actions = ['directory']) | |
271 | files = Compgen(actions = ['file']) | |
272 | def strings(*ss): return Compgen(ss) | |
273 | class patch_range(CompgenBase): | |
274 | def __init__(self, *endpoints): | |
275 | self.__endpoints = endpoints | |
276 | def words(self, var): | |
277 | words = set() | |
278 | for e in self.__endpoints: | |
279 | assert not e.actions(var) | |
280 | words |= e.words(var) | |
281 | return set(['$(_patch_range "%s" "%s")' % (' '.join(words), var)]) |