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