chiark / gitweb /
db008d08aa6a47fe8f141ec5d20fc9143e3dc682
[stgit] / stgit / argparse.py
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)."""
4
5 import optparse, sys, textwrap
6 from stgit import utils
7 from stgit.config import config
8
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."""
29     def __init__(self, *pargs, **kwargs):
30         self.pargs = pargs
31         self.kwargs = kwargs
32     def get_option(self):
33         kwargs = dict(self.kwargs)
34         kwargs['help'] = kwargs['short']
35         for k in ['short', 'long', 'args']:
36             kwargs.pop(k, None)
37         return optparse.make_option(*self.pargs, **kwargs)
38     def metavar(self):
39         o = self.get_option()
40         if not o.takes_value():
41             return None
42         if o.metavar:
43             return o.metavar
44         for flag in self.pargs:
45             if flag.startswith('--'):
46                 return utils.strip_prefix('--', flag).upper()
47         raise Exception('Cannot determine metavar')
48     def write_asciidoc(self, f):
49         for flag in self.pargs:
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')
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)
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)
106     f.write('Part of the StGit suite - see linkman:stg[1]\n')
107
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
114     return [
115         opt('--sign', action = 'callback', dest = 'sign_str', args = [],
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."""),
119         opt('--ack', action = 'callback', dest = 'sign_str', args = [],
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.""")]
123
124 def message_options(save_template):
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):
130         if (save_template and parser.values.message != None
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)
158     opts = [
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,
163             dest = 'message', type = 'string', args = [files],
164             short = 'Use FILE instead of invoking the editor', long = """
165             Use the contents of FILE instead of invoking the editor.
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.)
175
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
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 = []
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',
193             args = [strings('-M', '-C')],
194             short = 'Extra options to pass to "git diff"')]
195
196 def _person_opts(person, short):
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')
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']])
219
220 def author_options():
221     return _person_opts('author', 'auth')
222
223 def keep_option():
224     return [opt('-k', '--keep', action = 'store_true',
225                 short = 'Keep the local changes',
226                 default = config.get('stgit.autokeep') == 'yes')]
227
228 def merged_option():
229     return [opt('-m', '--merged', action = 'store_true',
230                 short = 'Check for patches merged upstream')]
231
232 class CompgenBase(object):
233     def actions(self, var): return set()
234     def words(self, var): return set()
235     def command(self, var):
236         cmd = ['compgen']
237         for act in self.actions(var):
238             cmd += ['-A', act]
239         words = self.words(var)
240         if words:
241             cmd += ['-W', '"%s"' % ' '.join(words)]
242         cmd += ['--', '"%s"' % var]
243         return ' '.join(cmd)
244
245 class CompgenJoin(CompgenBase):
246     def __init__(self, a, b):
247         assert isinstance(a, CompgenBase)
248         assert isinstance(b, CompgenBase)
249         self.__a = a
250         self.__b = b
251     def words(self, var): return self.__a.words(var) | self.__b.words(var)
252     def actions(self, var): return self.__a.actions(var) | self.__b.actions(var)
253
254 class Compgen(CompgenBase):
255     def __init__(self, words = frozenset(), actions = frozenset()):
256         self.__words = set(words)
257         self.__actions = set(actions)
258     def actions(self, var): return self.__actions
259     def words(self, var): return self.__words
260
261 def compjoin(compgens):
262     comp = Compgen()
263     for c in compgens:
264         comp = CompgenJoin(comp, c)
265     return comp
266
267 all_branches = Compgen(['$(_all_branches)'])
268 stg_branches = Compgen(['$(_stg_branches)'])
269 applied_patches = Compgen(['$(_applied_patches)'])
270 other_applied_patches = Compgen(['$(_other_applied_patches)'])
271 unapplied_patches = Compgen(['$(_unapplied_patches)'])
272 hidden_patches = Compgen(['$(_hidden_patches)'])
273 commit = Compgen(['$(_all_branches) $(_tags) $(_remotes)'])
274 conflicting_files = Compgen(['$(_conflicting_files)'])
275 dirty_files = Compgen(['$(_dirty_files)'])
276 unknown_files = Compgen(['$(_unknown_files)'])
277 known_files = Compgen(['$(_known_files)'])
278 repo = Compgen(actions = ['directory'])
279 dir = Compgen(actions = ['directory'])
280 files = Compgen(actions = ['file'])
281 def strings(*ss): return Compgen(ss)
282 class patch_range(CompgenBase):
283     def __init__(self, *endpoints):
284         self.__endpoints = endpoints
285     def words(self, var):
286         words = set()
287         for e in self.__endpoints:
288             assert not e.actions(var)
289             words |= e.words(var)
290         return set(['$(_patch_range "%s" "%s")' % (' '.join(words), var)])