1a7b1b36c2f11463ef99df3a0c2afb4e6914fef9
[stgit] / stgit / commands / edit.py
1 """Patch editing command
2 """
3
4 __copyright__ = """
5 Copyright (C) 2007, Catalin Marinas <catalin.marinas@gmail.com>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License version 2 as
9 published by the Free Software Foundation.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 """
20
21 from optparse import OptionParser, make_option
22 from email.Utils import formatdate
23
24 from stgit.commands.common import *
25 from stgit.utils import *
26 from stgit.out import *
27 from stgit import stack, git
28
29
30 help = 'edit a patch description or diff'
31 usage = """%prog [options] [<patch>]
32
33 Edit the given patch (defaulting to the current one) description,
34 author information or its diff (if the '--diff' option is
35 passed). Without any other option, the command invokes the editor with
36 the patch description and diff in the form below:
37
38 Subject line
39
40 From: author information
41 Date: creation date
42
43 Patch description
44
45 Signed-off-by: author
46
47 Command-line options can be used to modify specific information
48 without invoking the editor.
49
50 If the patch diff is edited but the patch application fails, the
51 rejected patch is stored in the .stgit-failed.patch file (and also in
52 .stgit-edit.{diff,txt}). The edited patch can be replaced with one of
53 these files using the '--file' and '--diff' options.
54 """
55
56 options = [make_option('-d', '--diff',
57 help = 'allow the editing of the patch diff',
58 action = 'store_true'),
59 make_option('-f', '--file',
60 help = 'use FILE instead of invoking the editor'),
61 make_option('-O', '--diff-opts',
62 help = 'options to pass to git-diff'),
63 make_option('--undo',
64 help = 'revert the commit generated by the last edit',
65 action = 'store_true'),
66 make_option('-a', '--annotate', metavar = 'NOTE',
67 help = 'annotate the patch log entry'),
68 make_option('-m', '--message',
69 help = 'replace the patch description with MESSAGE'),
70 make_option('--author', metavar = '"NAME <EMAIL>"',
71 help = 'replae the author details with "NAME <EMAIL>"'),
72 make_option('--authname',
73 help = 'replace the author name with AUTHNAME'),
74 make_option('--authemail',
75 help = 'replace the author e-mail with AUTHEMAIL'),
76 make_option('--authdate',
77 help = 'replace the author date with AUTHDATE'),
78 make_option('--commname',
79 help = 'replace the committer name with COMMNAME'),
80 make_option('--commemail',
81 help = 'replace the committer e-mail with COMMEMAIL')
82 ] + make_sign_options()
83
84 def __update_patch(pname, fname, options):
85 """Update the current patch from the given file.
86 """
87 patch = crt_series.get_patch(pname)
88
89 bottom = patch.get_bottom()
90 top = patch.get_top()
91
92 f = open(fname)
93 message, author_name, author_email, author_date, diff = parse_patch(f)
94 f.close()
95
96 if options.diff:
97 git.switch(bottom)
98 try:
99 git.apply_patch(fname)
100 except:
101 # avoid inconsistent repository state
102 git.switch(top)
103 raise
104
105 out.start('Updating patch "%s"' % pname)
106 crt_series.refresh_patch(message = message,
107 author_name = author_name,
108 author_email = author_email,
109 author_date = author_date,
110 backup = True, log = 'edit')
111 if crt_series.empty_patch(pname):
112 out.done('empty patch')
113 else:
114 out.done()
115
116 def __edit_update_patch(pname, options):
117 """Edit the given patch interactively.
118 """
119 patch = crt_series.get_patch(pname)
120
121 if options.diff_opts:
122 if not options.diff:
123 raise CmdException, '--diff-opts only available with --diff'
124 diff_flags = options.diff_opts.split()
125 else:
126 diff_flags = []
127
128 # generate the file to be edited
129 descr = patch.get_description().strip()
130 descr_lines = descr.split('\n')
131 authdate = patch.get_authdate()
132
133 short_descr = descr_lines[0].rstrip()
134 long_descr = reduce(lambda x, y: x + '\n' + y,
135 descr_lines[1:], '').strip()
136
137 tmpl = '%(shortdescr)s\n\n' \
138 'From: %(authname)s <%(authemail)s>\n'
139 if authdate:
140 tmpl += 'Date: %(authdate)s\n'
141 tmpl += '\n%(longdescr)s\n'
142
143 tmpl_dict = {
144 'shortdescr': short_descr,
145 'longdescr': long_descr,
146 'authname': patch.get_authname(),
147 'authemail': patch.get_authemail(),
148 'authdate': patch.get_authdate()
149 }
150
151 if options.diff:
152 # add the patch diff to the edited file
153 bottom = patch.get_bottom()
154 top = patch.get_top()
155
156 tmpl += '---\n\n' \
157 '%(diffstat)s\n' \
158 '%(diff)s'
159
160 tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
161 tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
162 diff_flags = diff_flags)
163
164 for key in tmpl_dict:
165 # make empty strings if key is not available
166 if tmpl_dict[key] is None:
167 tmpl_dict[key] = ''
168
169 text = tmpl % tmpl_dict
170
171 if options.diff:
172 fname = '.stgit-edit.diff'
173 else:
174 fname = '.stgit-edit.txt'
175
176 # write the file to be edited
177 f = open(fname, 'w+')
178 f.write(text)
179 f.close()
180
181 # invoke the editor
182 call_editor(fname)
183
184 __update_patch(pname, fname, options)
185
186 def func(parser, options, args):
187 """Edit the given patch or the current one.
188 """
189 crt_pname = crt_series.get_current()
190
191 if not args:
192 pname = crt_pname
193 if not pname:
194 raise CmdException, 'No patches applied'
195 elif len(args) == 1:
196 pname = args[0]
197 if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
198 raise CmdException, 'Cannot edit unapplied or hidden patches'
199 elif not crt_series.patch_applied(pname):
200 raise CmdException, 'Unknown patch "%s"' % pname
201 else:
202 parser.error('incorrect number of arguments')
203
204 check_local_changes()
205 check_conflicts()
206 check_head_top_equal()
207
208 if pname != crt_pname:
209 # Go to the patch to be edited
210 applied = crt_series.get_applied()
211 between = applied[:applied.index(pname):-1]
212 pop_patches(between)
213
214 if options.author:
215 options.authname, options.authemail = name_email(options.author)
216
217 if options.undo:
218 out.start('Undoing the editing of "%s"' % pname)
219 crt_series.undo_refresh()
220 out.done()
221 elif options.message or options.authname or options.authemail \
222 or options.authdate or options.commname or options.commemail \
223 or options.sign_str:
224 # just refresh the patch with the given information
225 out.start('Updating patch "%s"' % pname)
226 crt_series.refresh_patch(message = options.message,
227 author_name = options.authname,
228 author_email = options.authemail,
229 author_date = options.authdate,
230 committer_name = options.commname,
231 committer_email = options.commemail,
232 backup = True, sign_str = options.sign_str,
233 log = 'edit',
234 notes = options.annotate)
235 out.done()
236 elif options.file:
237 __update_patch(pname, options.file, options)
238 else:
239 __edit_update_patch(pname, options)
240
241 if pname != crt_pname:
242 # Push the patches back
243 between.reverse()
244 push_patches(between)