2e8ae372490d8d48c2b8a50d5f4945ec5d87c0b3
[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('-O', '--diff-opts',
65 help = 'options to pass to git-diff'),
66 make_option('--undo',
67 help = 'revert the commit generated by the last edit',
68 action = 'store_true'),
69 make_option('-a', '--annotate', metavar = 'NOTE',
70 help = 'annotate the patch log entry'),
71 make_option('--author', metavar = '"NAME <EMAIL>"',
72 help = 'replae the author details with "NAME <EMAIL>"'),
73 make_option('--authname',
74 help = 'replace the author name with AUTHNAME'),
75 make_option('--authemail',
76 help = 'replace the author e-mail with AUTHEMAIL'),
77 make_option('--authdate',
78 help = 'replace the author date with AUTHDATE'),
79 make_option('--commname',
80 help = 'replace the committer name with COMMNAME'),
81 make_option('--commemail',
82 help = 'replace the committer e-mail with COMMEMAIL')
83 ] + make_sign_options() + make_message_options()
84
85 def __update_patch(pname, text, options):
86 """Update the current patch from the given text.
87 """
88 patch = crt_series.get_patch(pname)
89
90 bottom = patch.get_bottom()
91 top = patch.get_top()
92
93 if text:
94 (message, author_name, author_email, author_date, diff
95 ) = parse_patch(text)
96 else:
97 message = author_name = author_email = author_date = diff = None
98
99 out.start('Updating patch "%s"' % pname)
100
101 if options.diff:
102 git.switch(bottom)
103 try:
104 git.apply_patch(diff = diff)
105 except:
106 # avoid inconsistent repository state
107 git.switch(top)
108 raise
109
110 def c(a, b):
111 if a != None:
112 return a
113 return b
114 crt_series.refresh_patch(message = message,
115 author_name = c(options.authname, author_name),
116 author_email = c(options.authemail, author_email),
117 author_date = c(options.authdate, author_date),
118 committer_name = options.commname,
119 committer_email = options.commemail,
120 backup = True, sign_str = options.sign_str,
121 log = 'edit', notes = options.annotate)
122
123 if crt_series.empty_patch(pname):
124 out.done('empty patch')
125 else:
126 out.done()
127
128 def __generate_file(pname, write_fn, options):
129 """Generate a file containing the description to edit
130 """
131 patch = crt_series.get_patch(pname)
132
133 if options.diff_opts:
134 if not options.diff:
135 raise CmdException, '--diff-opts only available with --diff'
136 diff_flags = options.diff_opts.split()
137 else:
138 diff_flags = []
139
140 # generate the file to be edited
141 descr = patch.get_description().strip()
142 authdate = patch.get_authdate()
143
144 tmpl = 'From: %(authname)s <%(authemail)s>\n'
145 if authdate:
146 tmpl += 'Date: %(authdate)s\n'
147 tmpl += '\n%(descr)s\n'
148
149 tmpl_dict = {
150 'descr': descr,
151 'authname': patch.get_authname(),
152 'authemail': patch.get_authemail(),
153 'authdate': patch.get_authdate()
154 }
155
156 if options.diff:
157 # add the patch diff to the edited file
158 bottom = patch.get_bottom()
159 top = patch.get_top()
160
161 tmpl += '---\n\n' \
162 '%(diffstat)s\n' \
163 '%(diff)s'
164
165 tmpl_dict['diffstat'] = git.diffstat(rev1 = bottom, rev2 = top)
166 tmpl_dict['diff'] = git.diff(rev1 = bottom, rev2 = top,
167 diff_flags = diff_flags)
168
169 for key in tmpl_dict:
170 # make empty strings if key is not available
171 if tmpl_dict[key] is None:
172 tmpl_dict[key] = ''
173
174 text = tmpl % tmpl_dict
175
176 # write the file to be edited
177 write_fn(text)
178
179 def __edit_update_patch(pname, options):
180 """Edit the given patch interactively.
181 """
182 if options.diff:
183 fname = '.stgit-edit.diff'
184 else:
185 fname = '.stgit-edit.txt'
186 def write_fn(text):
187 f = file(fname, 'w')
188 f.write(text)
189 f.close()
190
191 __generate_file(pname, write_fn, options)
192
193 # invoke the editor
194 call_editor(fname)
195
196 __update_patch(pname, file(fname).read(), options)
197
198 def func(parser, options, args):
199 """Edit the given patch or the current one.
200 """
201 crt_pname = crt_series.get_current()
202
203 if not args:
204 pname = crt_pname
205 if not pname:
206 raise CmdException, 'No patches applied'
207 elif len(args) == 1:
208 pname = args[0]
209 if crt_series.patch_unapplied(pname) or crt_series.patch_hidden(pname):
210 raise CmdException, 'Cannot edit unapplied or hidden patches'
211 elif not crt_series.patch_applied(pname):
212 raise CmdException, 'Unknown patch "%s"' % pname
213 else:
214 parser.error('incorrect number of arguments')
215
216 check_local_changes()
217 check_conflicts()
218 check_head_top_equal(crt_series)
219
220 if pname != crt_pname:
221 # Go to the patch to be edited
222 applied = crt_series.get_applied()
223 between = applied[:applied.index(pname):-1]
224 pop_patches(crt_series, between)
225
226 if options.author:
227 options.authname, options.authemail = name_email(options.author)
228
229 if options.undo:
230 out.start('Undoing the editing of "%s"' % pname)
231 crt_series.undo_refresh()
232 out.done()
233 elif options.save_template:
234 __generate_file(pname, options.save_template, options)
235 elif any([options.message, options.authname, options.authemail,
236 options.authdate, options.commname, options.commemail,
237 options.sign_str]):
238 out.start('Updating patch "%s"' % pname)
239 __update_patch(pname, options.message, options)
240 out.done()
241 else:
242 __edit_update_patch(pname, options)
243
244 if pname != crt_pname:
245 # Push the patches back
246 between.reverse()
247 push_patches(crt_series, between)