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