Added --save-template flag to edit command
[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 description and author information of the given patch (or the
34 current patch if no patch name was given). With --diff, also edit the
35 diff.
36
37 The editor is invoked with the following contents:
38
39 From: A U Thor <author@example.com>
40 Date: creation date
41
42 Patch description
43
44 If --diff was specified, the diff appears at the bottom, after a
45 separator:
46
47 ---
48
49 Diff text
50
51 Command-line options can be used to modify specific information
52 without invoking the editor.
53
54 If the patch diff is edited but the patch application fails, the
55 rejected patch is stored in the .stgit-failed.patch file (and also in
56 .stgit-edit.{diff,txt}). The edited patch can be replaced with one of
57 these files using the '--file' and '--diff' options.
58 """
59
60 directory = DirectoryGotoToplevel()
61 options = [make_option('-d', '--diff',
62 help = 'edit the patch diff',
63 action = 'store_true'),
64 make_option('-f', '--file',
65 help = 'use FILE instead of invoking the editor'),
66 make_option('-O', '--diff-opts',
67 help = 'options to pass to git-diff'),
68 make_option('--undo',
69 help = 'revert the commit generated by the last edit',
70 action = 'store_true'),
71 make_option('-a', '--annotate', metavar = 'NOTE',
72 help = 'annotate the patch log entry'),
73 make_option('-m', '--message',
74 help = 'replace the patch description with MESSAGE'),
75 make_option('--save-template', metavar = 'FILE',
76 help = 'save the patch to FILE in the format used by -f'),
77 make_option('--author', metavar = '"NAME <EMAIL>"',
78 help = 'replae the author details with "NAME <EMAIL>"'),
79 make_option('--authname',
80 help = 'replace the author name with AUTHNAME'),
81 make_option('--authemail',
82 help = 'replace the author e-mail with AUTHEMAIL'),
83 make_option('--authdate',
84 help = 'replace the author date with AUTHDATE'),
85 make_option('--commname',
86 help = 'replace the committer name with COMMNAME'),
87 make_option('--commemail',
88 help = 'replace the committer e-mail with COMMEMAIL')
89 ] + make_sign_options()
90
91 def __update_patch(pname, fname, options):
92 """Update the current patch from the given file.
93 """
94 patch = crt_series.get_patch(pname)
95
96 bottom = patch.get_bottom()
97 top = patch.get_top()
98
99 f = open(fname)
100 message, author_name, author_email, author_date, diff = parse_patch(f)
101 f.close()
102
103 out.start('Updating patch "%s"' % pname)
104
105 if options.diff:
106 git.switch(bottom)
107 try:
108 git.apply_patch(fname)
109 except:
110 # avoid inconsistent repository state
111 git.switch(top)
112 raise
113
114 crt_series.refresh_patch(message = message,
115 author_name = author_name,
116 author_email = author_email,
117 author_date = author_date,
118 backup = True, log = 'edit')
119
120 if crt_series.empty_patch(pname):
121 out.done('empty patch')
122 else:
123 out.done()
124
125 def __generate_file(pname, fname, options):
126 """Generate a file containing the description to edit
127 """
128 patch = crt_series.get_patch(pname)
129
130 if options.diff_opts:
131 if not options.diff:
132 raise CmdException, '--diff-opts only available with --diff'
133 diff_flags = options.diff_opts.split()
134 else:
135 diff_flags = []
136
137 # generate the file to be edited
138 descr = patch.get_description().strip()
139 authdate = patch.get_authdate()
140
141 tmpl = 'From: %(authname)s <%(authemail)s>\n'
142 if authdate:
143 tmpl += 'Date: %(authdate)s\n'
144 tmpl += '\n%(descr)s\n'
145
146 tmpl_dict = {
147 'descr': descr,
148 'authname': patch.get_authname(),
149 'authemail': patch.get_authemail(),
150 'authdate': patch.get_authdate()
151 }
152
153 if options.diff:
154 # add the patch diff to the edited file
155 bottom = patch.get_bottom()
156 top = patch.get_top()
157
158 tmpl += '---\n\n' \
159 '%(diffstat)s\n' \
160 '%(diff)s'
161
162 tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
163 tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
164 diff_flags = diff_flags)
165
166 for key in tmpl_dict:
167 # make empty strings if key is not available
168 if tmpl_dict[key] is None:
169 tmpl_dict[key] = ''
170
171 text = tmpl % tmpl_dict
172
173 # write the file to be edited
174 f = open(fname, 'w+')
175 f.write(text)
176 f.close()
177
178 def __edit_update_patch(pname, options):
179 """Edit the given patch interactively.
180 """
181 if options.diff:
182 fname = '.stgit-edit.diff'
183 else:
184 fname = '.stgit-edit.txt'
185
186 __generate_file(pname, fname, options)
187
188 # invoke the editor
189 call_editor(fname)
190
191 __update_patch(pname, fname, options)
192
193 def func(parser, options, args):
194 """Edit the given patch or the current one.
195 """
196 crt_pname = crt_series.get_current()
197
198 if not args:
199 pname = crt_pname
200 if not pname:
201 raise CmdException, 'No patches applied'
202 elif len(args) == 1:
203 pname = args[0]
204 if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
205 raise CmdException, 'Cannot edit unapplied or hidden patches'
206 elif not crt_series.patch_applied(pname):
207 raise CmdException, 'Unknown patch "%s"' % pname
208 else:
209 parser.error('incorrect number of arguments')
210
211 check_local_changes()
212 check_conflicts()
213 check_head_top_equal(crt_series)
214
215 if pname != crt_pname:
216 # Go to the patch to be edited
217 applied = crt_series.get_applied()
218 between = applied[:applied.index(pname):-1]
219 pop_patches(crt_series, between)
220
221 if options.author:
222 options.authname, options.authemail = name_email(options.author)
223
224 if options.undo:
225 out.start('Undoing the editing of "%s"' % pname)
226 crt_series.undo_refresh()
227 out.done()
228 elif options.message or options.authname or options.authemail \
229 or options.authdate or options.commname or options.commemail \
230 or options.sign_str:
231 # just refresh the patch with the given information
232 out.start('Updating patch "%s"' % pname)
233 crt_series.refresh_patch(message = options.message,
234 author_name = options.authname,
235 author_email = options.authemail,
236 author_date = options.authdate,
237 committer_name = options.commname,
238 committer_email = options.commemail,
239 backup = True, sign_str = options.sign_str,
240 log = 'edit',
241 notes = options.annotate)
242 out.done()
243 elif options.save_template:
244 __generate_file(pname, options.save_template, options)
245 elif options.file:
246 __update_patch(pname, options.file, options)
247 else:
248 __edit_update_patch(pname, options)
249
250 if pname != crt_pname:
251 # Push the patches back
252 between.reverse()
253 push_patches(crt_series, between)