chiark / gitweb /
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)