Infrastructure for current directory 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 = DirectoryHasRepository()
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('--author', metavar = '"NAME <EMAIL>"',
76 help = 'replae the author details with "NAME <EMAIL>"'),
77 make_option('--authname',
78 help = 'replace the author name with AUTHNAME'),
79 make_option('--authemail',
80 help = 'replace the author e-mail with AUTHEMAIL'),
81 make_option('--authdate',
82 help = 'replace the author date with AUTHDATE'),
83 make_option('--commname',
84 help = 'replace the committer name with COMMNAME'),
85 make_option('--commemail',
86 help = 'replace the committer e-mail with COMMEMAIL')
87 ] + make_sign_options()
88
89 def __update_patch(pname, fname, options):
90 """Update the current patch from the given file.
91 """
92 patch = crt_series.get_patch(pname)
93
94 bottom = patch.get_bottom()
95 top = patch.get_top()
96
97 f = open(fname)
98 message, author_name, author_email, author_date, diff = parse_patch(f)
99 f.close()
100
101 out.start('Updating patch "%s"' % pname)
102
103 if options.diff:
104 git.switch(bottom)
105 try:
106 git.apply_patch(fname)
107 except:
108 # avoid inconsistent repository state
109 git.switch(top)
110 raise
111
112 crt_series.refresh_patch(message = message,
113 author_name = author_name,
114 author_email = author_email,
115 author_date = author_date,
116 backup = True, log = 'edit')
117
118 if crt_series.empty_patch(pname):
119 out.done('empty patch')
120 else:
121 out.done()
122
123 def __edit_update_patch(pname, options):
124 """Edit the given patch interactively.
125 """
126 patch = crt_series.get_patch(pname)
127
128 if options.diff_opts:
129 if not options.diff:
130 raise CmdException, '--diff-opts only available with --diff'
131 diff_flags = options.diff_opts.split()
132 else:
133 diff_flags = []
134
135 # generate the file to be edited
136 descr = patch.get_description().strip()
137 authdate = patch.get_authdate()
138
139 tmpl = 'From: %(authname)s <%(authemail)s>\n'
140 if authdate:
141 tmpl += 'Date: %(authdate)s\n'
142 tmpl += '\n%(descr)s\n'
143
144 tmpl_dict = {
145 'descr': 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)